import contextlib
import pickle
import sqlite3
import sys
import threading
import time
from collections.abc import Generator
from pathlib import Path
from typing import Any
from unittest.mock import patch

import pytest

from codeflash_python.benchmarking._trace_models import FakeCode, FakeFrame
from codeflash_python.benchmarking._tracing import Tracer


class TestFakeCode:
    def test_fake_code_initialization(self) -> None:
        fake_code = FakeCode("test.py", 10, "test_function")
        assert fake_code.co_filename == "test.py"
        assert fake_code.co_line == 10
        assert fake_code.co_name == "test_function"
        assert fake_code.co_firstlineno == 0

    def test_fake_code_repr(self) -> None:
        fake_code = FakeCode("test.py", 10, "test_function")
        expected_repr = repr(("test.py", 10, "test_function", None))
        assert repr(fake_code) == expected_repr


class TestFakeFrame:
    def test_fake_frame_initialization(self) -> None:
        fake_code = FakeCode("test.py", 10, "test_function")
        fake_frame = FakeFrame(fake_code, None)
        assert fake_frame.f_code == fake_code
        assert fake_frame.f_back is None
        assert fake_frame.f_locals == {}

    def test_fake_frame_with_prior(self) -> None:
        fake_code1 = FakeCode("test1.py", 5, "func1")
        fake_code2 = FakeCode("test2.py", 10, "func2")
        fake_frame1 = FakeFrame(fake_code1, None)
        fake_frame2 = FakeFrame(fake_code2, fake_frame1)

        assert fake_frame2.f_code == fake_code2
        assert fake_frame2.f_back == fake_frame1


def _make_tracer(
    tmp_path: Path,
    project_root: Path,
    tests_root: Path,
    *,
    command: str = "pytest random",
    **kwargs: Any,
) -> Tracer:
    """Build a Tracer using the new constructor interface."""
    return Tracer(
        project_root=project_root,
        module_root=project_root,
        tests_root=tests_root,
        output_file=tmp_path / "trace_file.trace",
        command=command,
        **kwargs,
    )


class TestTracer:
    @pytest.fixture
    def tracer_env(self, tmp_path: Path) -> dict[str, Any]:
        """Return the paths needed to build a Tracer."""
        tests_dir = tmp_path / "tests"
        tests_dir.mkdir(exist_ok=True)
        current_dir = Path.cwd()
        return {
            "tmp_path": tmp_path,
            "project_root": current_dir,
            "tests_root": tests_dir,
            "result_pickle_file_path": tmp_path / "replay_test.pkl",
        }

    @pytest.fixture(autouse=True)
    def reset_tracer_state(self) -> Generator[None, None, None]:
        """Reset the tracer used_once state before each test."""
        Tracer.used_once = False
        yield
        Tracer.used_once = False

    def test_tracer_disabled_by_environment(
        self, tracer_env: dict[str, Any]
    ) -> None:
        """Test that tracer is disabled when CODEFLASH_TRACER_DISABLE is set."""
        with patch.dict("os.environ", {"CODEFLASH_TRACER_DISABLE": "1"}):
            tracer = _make_tracer(
                tracer_env["tmp_path"],
                tracer_env["project_root"],
                tracer_env["tests_root"],
            )
            assert tracer.disable is True

    def test_tracer_disabled_with_existing_profiler(
        self, tracer_env: dict[str, Any]
    ) -> None:
        """Test that tracer is disabled when another profiler is running."""

        def dummy_profiler(
            _frame: object, _event: str, _arg: object
        ) -> object:
            return dummy_profiler

        sys.setprofile(dummy_profiler)
        try:
            tracer = _make_tracer(
                tracer_env["tmp_path"],
                tracer_env["project_root"],
                tracer_env["tests_root"],
            )
            assert tracer.disable is True
        finally:
            sys.setprofile(None)

    def test_tracer_initialization_normal(
        self, tracer_env: dict[str, Any]
    ) -> None:
        """Test normal tracer initialization."""
        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
            functions=["test_func"],
            max_function_count=128,
            timeout=10,
        )

        assert tracer.disable is False
        assert tracer.functions == ["test_func"]
        assert tracer.max_function_count == 128
        assert tracer.timeout == 10
        assert hasattr(tracer, "_db_lock")
        assert tracer._db_lock is not None

    def test_tracer_timeout_validation(
        self, tracer_env: dict[str, Any]
    ) -> None:
        with pytest.raises(AssertionError):
            _make_tracer(
                tracer_env["tmp_path"],
                tracer_env["project_root"],
                tracer_env["tests_root"],
                timeout=0,
            )

        with pytest.raises(AssertionError):
            _make_tracer(
                tracer_env["tmp_path"],
                tracer_env["project_root"],
                tracer_env["tests_root"],
                timeout=-5,
            )

    def test_tracer_context_manager_disabled(
        self, tracer_env: dict[str, Any]
    ) -> None:
        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
        )
        tracer.disable = True

        with tracer:
            pass

        assert tracer.disable is True

    def test_tracer_function_filtering(
        self, tracer_env: dict[str, Any]
    ) -> None:
        """Test that tracer respects function filtering."""

        def test_function() -> int:
            return 42

        def other_function() -> int:
            return 24

        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
            functions=["test_function"],
        )

        with tracer:
            test_function()
            other_function()

        if tracer.output_file.exists():
            con = sqlite3.connect(tracer.output_file)
            cursor = con.cursor()

            cursor.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='function_calls'"
            )
            if cursor.fetchone():
                cursor.execute(
                    "SELECT function FROM function_calls WHERE function = 'test_function'"
                )
                cursor.fetchall()

                cursor.execute(
                    "SELECT function FROM function_calls WHERE function = 'other_function'"
                )
                cursor.fetchall()

            con.close()

    def test_tracer_max_function_count(
        self, tracer_env: dict[str, Any]
    ) -> None:
        def counting_function(n: int) -> int:
            return n * 2

        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
            max_function_count=3,
        )

        with tracer:
            for i in range(5):
                counting_function(i)

        assert tracer.trace_count <= 3, (
            "Tracer should limit the number of traced functions to max_function_count"
        )

    def test_tracer_timeout_functionality(
        self, tracer_env: dict[str, Any]
    ) -> None:
        def slow_function() -> str:
            time.sleep(0.1)
            return "done"

        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
            timeout=1,
        )

        with tracer:
            slow_function()

    def test_tracer_threading_safety(self, tracer_env: dict[str, Any]) -> None:
        """Test that tracer works correctly with threading."""
        results = []

        def thread_function(n: int) -> None:
            results.append(n * 2)

        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
        )

        with tracer:
            threads = []
            for i in range(3):
                thread = threading.Thread(target=thread_function, args=(i,))
                threads.append(thread)
                thread.start()

            for thread in threads:
                thread.join()

        assert 3 == len(results)

    def test_simulate_call(self, tracer_env: dict[str, Any]) -> None:
        """Test the simulate_call method."""
        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
        )

        tracer.simulate_call("test_simulation")

    def test_simulate_cmd_complete(self, tracer_env: dict[str, Any]) -> None:
        """Test the simulate_cmd_complete method."""
        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
        )

        tracer.simulate_call("test")
        tracer.simulate_cmd_complete()

    def test_runctx_method(self, tracer_env: dict[str, Any]) -> None:
        """Test the runctx method for executing code with tracing."""
        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
        )

        global_vars = {"x": 10}
        local_vars: dict[str, Any] = {}

        result = tracer.runctx("y = x * 2", global_vars, local_vars)

        assert result == tracer
        assert 20 == local_vars["y"]

    def test_tracer_handles_class_methods(
        self, tracer_env: dict[str, Any]
    ) -> None:
        """Test that tracer correctly handles class methods without crashing."""

        class TracerTestClass:
            def instance_method(self) -> str:
                return "instance"

            @classmethod
            def class_method(cls) -> str:
                return "class"

            @staticmethod
            def static_method() -> str:
                return "static"

        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
        )

        # The core assertion: class methods must not crash the tracer
        with tracer:
            obj = TracerTestClass()
            obj.instance_method()
            TracerTestClass.class_method()
            TracerTestClass.static_method()

        # If we get here without exceptions, the tracer handled all method types

    def test_tracer_handles_exceptions_gracefully(
        self, tracer_env: dict[str, Any]
    ) -> None:
        """Test that tracer handles exceptions in traced code gracefully."""

        def failing_function() -> None:
            raise ValueError("Test exception")

        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
        )

        with tracer, contextlib.suppress(ValueError):
            failing_function()

    def test_tracer_with_complex_arguments(
        self, tracer_env: dict[str, Any]
    ) -> None:
        def complex_function(
            data_dict: dict[str, Any],
            nested_list: list[list[int]],
            func_arg: object = lambda x: x,
        ) -> int:
            return len(data_dict) + len(nested_list)

        tracer = _make_tracer(
            tracer_env["tmp_path"],
            tracer_env["project_root"],
            tracer_env["tests_root"],
        )

        expected_dict = {"key": "value", "nested": {"inner": "data"}}
        expected_list = [[1, 2], [3, 4], [5, 6]]

        def expected_func(x: int) -> int:
            return x * 2

        with tracer:
            complex_function(
                expected_dict, expected_list, func_arg=expected_func
            )

        # Verify the tracer produced a valid SQLite trace file
        if tracer.output_file.exists():
            con = sqlite3.connect(tracer.output_file)
            cursor = con.cursor()

            cursor.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='function_calls'"
            )
            has_table = cursor.fetchone() is not None

            if has_table:
                cursor.execute(
                    "SELECT args FROM function_calls WHERE function = 'complex_function'"
                )
                result = cursor.fetchone()

                if result is not None:
                    traced_args = pickle.loads(result[0])

                    assert "data_dict" in traced_args
                    assert traced_args["data_dict"] == expected_dict
                    assert traced_args["nested_list"] == expected_list

            con.close()
