#
# Copyright (c) 2021 the Hugging Face team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.#
import os
import warnings
from typing import Tuple

import alembic
import pytest
from alembic.config import Config
from asgi_lifespan import LifespanManager
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from databases import Database
from fastapi import FastAPI
from httpx import AsyncClient

from app.api.routes.experiments import create_new_experiment, update_experiment_by_id
from app.db.repositories.experiments import ExperimentsRepository
from app.models.experiment import ExperimentCreatePublic, ExperimentPublic, ExperimentUpdate
from app.models.experiment_join import ExperimentJoinInput
from app.services.authentication import MoonlandingUser, Organization, RepoRole


# Apply migrations at beginning and end of testing session
@pytest.fixture(scope="session")
def apply_migrations():
    warnings.filterwarnings("ignore", category=DeprecationWarning)
    os.environ["TESTING"] = "1"
    config = Config("alembic.ini")
    alembic.command.upgrade(config, "head")
    yield
    alembic.command.downgrade(config, "base")


# Create a new application for testing
@pytest.fixture
def app(apply_migrations: None) -> FastAPI:
    from app.api.server import get_application

    app = get_application()

    return app


# Grab a reference to our database when needed
@pytest.fixture
def db(app: FastAPI) -> Database:
    return app.state._db


# Make requests in our tests
@pytest.fixture
async def client(app: FastAPI) -> AsyncClient:
    async with LifespanManager(app):
        async with AsyncClient(
            app=app, base_url="http://testserver", headers={"Content-Type": "application/json"}
        ) as client:
            yield client


# Fixtures for authenticated User1

# Create an organization
@pytest.fixture
def organization_1_admin():
    return Organization(name="org_1", role_in_org=RepoRole.admin)


@pytest.fixture
def organization_3_admin():
    return Organization(name="org_3", role_in_org=RepoRole.admin)


@pytest.fixture
def organization_a_admin():
    return Organization(name="organization_a", role_in_org=RepoRole.admin)


@pytest.fixture
def Organization_a_admin():
    return Organization(name="Organization_a", role_in_org=RepoRole.admin)


@pytest.fixture
def organization_1_read():
    return Organization(name="org_1", role_in_org=RepoRole.read)


@pytest.fixture
def organization_2_read():
    return Organization(name="org_2", role_in_org=RepoRole.read)


# Create a user for testing
@pytest.fixture
def moonlanding_user_1(
    organization_1_admin, Organization_a_admin, organization_a_admin, organization_2_read
) -> MoonlandingUser:
    moonlanding_user_1 = MoonlandingUser(
        username="User1",
        email="user1@test.co",
        orgs=[organization_1_admin, Organization_a_admin, organization_a_admin, organization_2_read],
    )
    return moonlanding_user_1


# Make requests in our tests
@pytest.fixture
async def client_wt_auth_user_1(app: FastAPI, moonlanding_user_1: MoonlandingUser) -> AsyncClient:
    from app.services.authentication import authenticate

    app.dependency_overrides[authenticate] = lambda: moonlanding_user_1
    async with LifespanManager(app):
        async with AsyncClient(
            app=app, base_url="http://testserver", headers={"Content-Type": "application/json"}
        ) as client:
            yield client


@pytest.fixture
async def test_experiment_1_created_by_user_1(db: Database, moonlanding_user_1: MoonlandingUser) -> ExperimentPublic:
    experiments_repo = ExperimentsRepository(db)

    organization_name = "org_1"
    model_name = "model_1"

    experiment = await experiments_repo.get_experiment_by_organization_and_model_name(
        organization_name=organization_name, model_name=model_name
    )

    if experiment:
        return experiment

    new_experiment = ExperimentCreatePublic(organization_name=organization_name, model_name=model_name)
    new_exp = await create_new_experiment(
        new_experiment=new_experiment,
        experiments_repo=experiments_repo,
        user=moonlanding_user_1,
    )
    return new_exp


@pytest.fixture
async def test_experiment_2_created_by_user_1_for_updates_tests(
    db: Database, moonlanding_user_1: MoonlandingUser
) -> ExperimentPublic:
    experiments_repo = ExperimentsRepository(db)

    organization_name = "organization_a"
    model_name = "model_1"

    experiment = await experiments_repo.get_experiment_by_organization_and_model_name(
        organization_name=organization_name, model_name=model_name
    )

    if experiment:
        return experiment

    new_experiment = ExperimentCreatePublic(organization_name=organization_name, model_name=model_name)
    new_exp = await create_new_experiment(
        new_experiment=new_experiment,
        experiments_repo=experiments_repo,
        user=moonlanding_user_1,
    )
    return new_exp


@pytest.fixture
async def test_experiment_join_input_1_by_user_2() -> Tuple[ExperimentJoinInput, rsa.RSAPrivateKey]:
    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    serialized_public_key = private_key.public_key().public_bytes(
        encoding=serialization.Encoding.OpenSSH, format=serialization.PublicFormat.OpenSSH
    )
    return ExperimentJoinInput(peer_public_key=serialized_public_key), private_key


@pytest.fixture
async def test_experiment_join_input_2_by_user_2() -> Tuple[ExperimentJoinInput, rsa.RSAPrivateKey]:
    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    serialized_public_key = private_key.public_key().public_bytes(
        encoding=serialization.Encoding.OpenSSH, format=serialization.PublicFormat.OpenSSH
    )
    return ExperimentJoinInput(peer_public_key=serialized_public_key), private_key


# Fixtures for authenticated User2

# Create a user for testing
@pytest.fixture
def moonlanding_user_2(organization_3_admin, organization_1_read) -> MoonlandingUser:
    moonlanding_user_2 = MoonlandingUser(
        username="User2", email="user2@test.co", orgs=[organization_1_read, organization_3_admin]
    )
    return moonlanding_user_2


@pytest.fixture
async def client_wt_auth_user_2(app: FastAPI, moonlanding_user_2: MoonlandingUser) -> AsyncClient:
    from app.services.authentication import authenticate

    app.dependency_overrides[authenticate] = lambda: moonlanding_user_2
    async with LifespanManager(app):
        async with AsyncClient(
            app=app, base_url="http://testserver", headers={"Content-Type": "application/json"}
        ) as client:
            yield client


@pytest.fixture
async def test_experiment_1_created_by_user_2(db: Database, moonlanding_user_2: MoonlandingUser) -> ExperimentPublic:
    experiments_repo = ExperimentsRepository(db)
    organization_name = "org_3"
    model_name = "model_1"

    experiment = await experiments_repo.get_experiment_by_organization_and_model_name(
        organization_name=organization_name, model_name=model_name
    )

    if experiment:
        return experiment

    new_experiment = ExperimentCreatePublic(organization_name=organization_name, model_name=model_name)

    experiment = await create_new_experiment(
        new_experiment=new_experiment,
        experiments_repo=experiments_repo,
        user=moonlanding_user_2,
    )
    exp = await update_experiment_by_id(
        id=experiment.id,
        experiment_update=ExperimentUpdate(coordinator_ip="192.0.2.0", coordinator_port=80),
        experiments_repo=experiments_repo,
        user=moonlanding_user_2,
    )
    return exp
