import glob
import importlib
import json
import os
import sys
import types
from collections import deque
from pathlib import Path
from typing import Any, Callable, Dict, Optional

import pytest

import agbenchmark.start_benchmark
from agbenchmark.utils.challenge import Challenge
from agbenchmark.utils.data_types import ChallengeData, SuiteConfig
from agbenchmark.utils.utils import get_test_path

DATA_CATEGORY = {}


def setup_dummy_dependencies(
    file_datum: list[dict[str, Any]],
    challenge_class: Any,
    challenge_data: ChallengeData,
) -> None:
    """Sets up the dependencies if it's a suite. Creates tests that pass
    based on the main test run."""

    def create_test_func(test_name: str) -> Callable[[Any, dict[str, Any]], None]:
        # This function will return another function

        # Define a dummy test function that does nothing
        def setup_dependency_test(self: Any, scores: dict[str, Any]) -> None:
            scores = self.get_dummy_scores(test_name, scores)
            assert scores == 1

        return setup_dependency_test

    for datum in file_datum:
        DATA_CATEGORY[datum["name"]] = challenge_data.category[0]
        test_func = create_test_func(datum["name"])
        # TODO: replace this once I figure out actual dependencies
        test_func = pytest.mark.depends(on=[challenge_data.name], name=datum["name"])(
            test_func
        )
        test_func = pytest.mark.parametrize(
            "challenge_data",
            [None],
            indirect=True,
        )(test_func)

        # Add category markers
        for category in challenge_data.category:
            test_func = getattr(pytest.mark, category)(test_func)

        test_func = pytest.mark.usefixtures("scores")(test_func)
        setattr(challenge_class, f"test_{datum['name']}", test_func)


def create_single_test(
    data: Dict[str, Any] | ChallengeData,
    challenge_location: str,
    file_datum: Optional[list[dict[str, Any]]] = None,
) -> None:
    challenge_data = None
    artifacts_location = None
    if isinstance(data, ChallengeData):
        challenge_data = data
        data = data.get_data()

    DATA_CATEGORY[data["name"]] = data["category"][0]

    # Define test class dynamically
    challenge_class = types.new_class(data["name"], (Challenge,))

    clean_challenge_location = get_test_path(challenge_location)
    setattr(challenge_class, "CHALLENGE_LOCATION", clean_challenge_location)

    # in the case of a suite
    if isinstance(challenge_data, ChallengeData):
        if file_datum:  # same task suite
            setup_dummy_dependencies(file_datum, challenge_class, challenge_data)

        artifacts_location = str(Path(challenge_location).resolve())
        if "--test" in sys.argv or "--maintain" in sys.argv or "--improve" in sys.argv:
            artifacts_location = str(Path(challenge_location).resolve().parent.parent)
        setattr(
            challenge_class,
            "_data_cache",
            {clean_challenge_location: challenge_data},
        )

    setattr(
        challenge_class,
        "ARTIFACTS_LOCATION",
        artifacts_location or str(Path(challenge_location).resolve().parent),
    )

    # Define test method within the dynamically created class
    @pytest.mark.asyncio
    async def test_method(self, config: Dict[str, Any], request) -> None:  # type: ignore
        # create a random number between 0 and 1
        test_name = self.data.name

        try:
            with open("challenges_already_beaten.json", "r") as f:
                challenges_beaten_in_the_past = json.load(f)
        except:
            challenges_beaten_in_the_past = {}

        if request.config.getoption("--explore") and challenges_beaten_in_the_past.get(
            test_name, False
        ):
            return None

        # skip optional categories
        self.skip_optional_categories(config)

        from helicone.lock import HeliconeLockManager

        if os.environ.get("HELICONE_API_KEY"):
            HeliconeLockManager.write_custom_property("challenge", self.data.name)

        cutoff = self.data.cutoff or 60

        timeout = cutoff
        if "--nc" in sys.argv:
            timeout = 100000
        if "--cutoff" in sys.argv:
            timeout = int(sys.argv[sys.argv.index("--cutoff") + 1])

        await self.setup_challenge(config, timeout)

        scores = self.get_scores(config)
        request.node.scores = scores  # store scores in request.node
        assert 1 in scores["values"]

    # Parametrize the method here
    test_method = pytest.mark.parametrize(
        "challenge_data",
        [data],
        indirect=True,
    )(test_method)

    setattr(challenge_class, "test_method", test_method)

    # Attach the new class to a module so it can be discovered by pytest
    module = importlib.import_module(__name__)
    setattr(module, data["name"], challenge_class)


def create_single_suite_challenge(challenge_data: ChallengeData, path: Path) -> None:
    create_single_test(challenge_data, str(path))


def create_challenge(
    data: Dict[str, Any],
    json_file: str,
    suite_config: SuiteConfig | None,
    json_files: deque,
) -> deque:
    path = Path(json_file).resolve()
    if suite_config is not None:
        grandparent_dir = path.parent.parent

        # if its a single test running we dont care about the suite
        if "--test" in sys.argv or "--maintain" in sys.argv or "--improve" in sys.argv:
            challenge_data = suite_config.challenge_from_test_data(data)
            create_single_suite_challenge(challenge_data, path)
            return json_files

        # Get all data.json files within the grandparent directory
        suite_files = suite_config.get_data_paths(grandparent_dir)

        # Remove all data.json files from json_files list, except for current_file
        json_files = deque(
            file
            for file in json_files
            if file not in suite_files
            and Path(file).resolve() != Path(json_file).resolve()
        )

        suite_file_datum = [
            ChallengeData.get_json_from_path(suite_file)
            for suite_file in suite_files
            if suite_file != json_file
        ]

        file_datum = [data, *suite_file_datum]

        if suite_config.same_task:
            challenge_data = suite_config.challenge_from_datum(file_datum)

            create_single_test(
                challenge_data, str(grandparent_dir), file_datum=file_datum
            )
        else:
            reverse = suite_config.reverse_order

            # TODO: reversing doesn't work, for the same reason why the ordering of dummy tests doesn't work
            if reverse:
                paired_data = list(reversed(list(zip(file_datum, suite_files))))
            else:
                paired_data = list(zip(file_datum, suite_files))

            for file_data, file_path in paired_data:
                # if we're running in reverse we don't want dependencies to get in the way
                if reverse:
                    file_data["dependencies"] = []
                create_single_test(file_data, file_path)

    else:
        create_single_test(data, str(path))

    return json_files


# if there's any suite.json files with that prefix


def generate_tests() -> None:  # sourcery skip: invert-any-all
    print("Generating tests...")

    json_files = deque(
        glob.glob(
            f"{agbenchmark.start_benchmark.CHALLENGES_PATH}/**/data.json",
            recursive=True,
        )
    )
    regression_tests = agbenchmark.start_benchmark.get_regression_data()

    # for suites to know if the file has already been used to generate the tests
    # Dynamic class creation
    while json_files:
        json_file = (
            json_files.popleft()
        )  # Take and remove the first element from json_files

        if challenge_should_be_ignored(json_file):
            continue
        data = ChallengeData.get_json_from_path(json_file)
        suite_config = SuiteConfig.suite_data_if_suite(Path(json_file))

        commands = sys.argv
        # --category flag
        if "--category" in commands:
            categories = data.get("category", [])
            commands_set = set(commands)

            # Add the shared category if the conditions are met
            if suite_config and suite_config.same_task:
                # handled by if same_task is false in types
                categories += suite_config.shared_category  # type: ignore

            # Convert the combined list to a set
            categories_set = set(categories)

            # If there's no overlap with commands
            if not categories_set.intersection(commands_set):
                continue

        # --test flag, only run the test if it's the exact one specified
        test_flag = "--test" in commands
        if test_flag and data["name"] not in commands:
            continue

        # --maintain and --improve flag
        in_regression = regression_tests.get(data["name"], None)
        improve_flag = in_regression and "--improve" in commands
        maintain_flag = not in_regression and "--maintain" in commands
        if "--maintain" in commands and maintain_flag:
            continue
        elif "--improve" in commands and improve_flag:
            continue

        # "--suite flag
        if "--suite" in commands:
            if not suite_config:
                # not a test from a suite
                continue
            elif not any(command in data["name"] for command in commands):
                continue

            # elif (
            #     not any(command in data["name"] for command in commands)
            #     and suite_config.prefix not in data["name"]
            # ):
            #     # a part of the suite but not the one specified
            #     continue

        json_files = create_challenge(data, json_file, suite_config, json_files)

        if suite_config and not (test_flag or maintain_flag or improve_flag):
            print(f"Generated suite for {suite_config.prefix}.")
        else:
            print(f"Generated test for {data['name']}.")


def challenge_should_be_ignored(json_file):
    return "challenges/deprecated" in json_file or "challenges/library" in json_file


generate_tests()
