"""Tests for concolic test utilities (_concolic module)."""

from __future__ import annotations

import os
import subprocess
import textwrap
from pathlib import Path
from unittest.mock import MagicMock, patch

from codeflash_python.testing._concolic import (
    CROSSHAIR_KNOWN_LIMITATION_PATTERNS,
    AssertCleanup,
    clean_concolic_tests,
    is_valid_concolic_test,
    make_env_with_project_root,
)


class TestCrosshairKnownLimitationPatterns:
    """CROSSHAIR_KNOWN_LIMITATION_PATTERNS constant."""

    def test_is_tuple(self) -> None:
        """The constant is a tuple."""
        assert isinstance(CROSSHAIR_KNOWN_LIMITATION_PATTERNS, tuple)

    def test_contains_expected_patterns(self) -> None:
        """Contains the three known limitation patterns."""
        assert "<locals>" in CROSSHAIR_KNOWN_LIMITATION_PATTERNS
        assert " object at 0x" in CROSSHAIR_KNOWN_LIMITATION_PATTERNS
        assert "<list_iterator" in CROSSHAIR_KNOWN_LIMITATION_PATTERNS

    def test_length(self) -> None:
        """Contains exactly three patterns."""
        assert 3 == len(CROSSHAIR_KNOWN_LIMITATION_PATTERNS)

    def test_all_strings(self) -> None:
        """Every element is a string."""
        for pattern in CROSSHAIR_KNOWN_LIMITATION_PATTERNS:
            assert isinstance(pattern, str)


class TestIsValidConcolicTest:
    """is_valid_concolic_test validation of concolic test code."""

    def test_syntax_error_returns_false(self) -> None:
        """Code that fails ast.parse returns False."""
        code = "def test_bad(:\n    pass"
        assert is_valid_concolic_test(code) is False

    def test_known_limitation_pattern_returns_false(self) -> None:
        """Code containing a known limitation pattern returns False."""
        code = textwrap.dedent("""\
            def test_locals():
                x = foo.<locals>.bar
        """)
        assert is_valid_concolic_test(code) is False

    def test_novel_syntax_error_returns_false(self) -> None:
        """Code with a non-limitation syntax error returns False."""
        code = "def test_bad() -> !!:\n    pass"
        assert is_valid_concolic_test(code) is False

    @patch("codeflash_python.testing._concolic.subprocess.run")
    def test_valid_but_failing_test_returns_false(
        self,
        mock_run: MagicMock,
    ) -> None:
        """Syntactically valid code that fails pytest returns False."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["pytest"],
            returncode=1,
            stdout="FAILED",
            stderr="",
        )
        code = textwrap.dedent("""\
            def test_fail():
                assert False
        """)
        assert is_valid_concolic_test(code) is False

    @patch("codeflash_python.testing._concolic.subprocess.run")
    def test_valid_passing_test_returns_true(
        self,
        mock_run: MagicMock,
    ) -> None:
        """Syntactically valid code that passes pytest returns True."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["pytest"],
            returncode=0,
            stdout="1 passed",
            stderr="",
        )
        code = textwrap.dedent("""\
            def test_pass():
                assert True
        """)
        assert is_valid_concolic_test(code) is True

    @patch("codeflash_python.testing._concolic.subprocess.run")
    def test_timeout_returns_false(
        self,
        mock_run: MagicMock,
    ) -> None:
        """Subprocess timeout is handled gracefully as False."""
        mock_run.side_effect = subprocess.TimeoutExpired(
            cmd=["pytest"],
            timeout=30,
        )
        code = textwrap.dedent("""\
            def test_slow():
                import time
                time.sleep(999)
        """)
        assert is_valid_concolic_test(code) is False

    def test_integration_valid_test_passes(
        self,
        tmp_path: Path,
    ) -> None:
        """Integration: a minimal passing test succeeds via real subprocess."""
        code = textwrap.dedent("""\
            def test_pass():
                assert True
        """)
        result = is_valid_concolic_test(code, project_root=str(tmp_path))
        assert result is True


class TestAssertCleanup:
    """AssertCleanup assert-to-call transformation."""

    def test_transform_asserts_basic(self) -> None:
        """Transforms assert func() == value to func()."""
        cleanup = AssertCleanup()
        code = "    assert func() == 42\n"
        result = cleanup.transform_asserts(code)
        assert "func()" in result
        assert "assert" not in result

    def test_transform_asserts_leaves_non_assert(self) -> None:
        """Non-assert lines are left unchanged."""
        cleanup = AssertCleanup()
        code = "    x = func()\n"
        result = cleanup.transform_asserts(code)
        assert "    x = func()" == result

    def test_transform_asserts_assert_not(self) -> None:
        """Handles assert not expr by producing not expr."""
        cleanup = AssertCleanup()
        code = "    assert not expr\n"
        result = cleanup.transform_asserts(code)
        assert "not expr" in result
        assert "assert " not in result

    def test_transform_assert_line_simple_assert(self) -> None:
        """Transforms assert func() to func()."""
        cleanup = AssertCleanup()
        result = cleanup._transform_assert_line("    assert func()")
        assert result is not None
        assert "func()" in result

    def test_transform_assert_line_assert_equal(self) -> None:
        """Transforms self.assertEqual(a, b) to a."""
        cleanup = AssertCleanup()
        result = cleanup._transform_assert_line("    self.assertEqual(a, b)")
        assert result is not None
        assert "a" in result

    def test_transform_assert_line_non_assert_returns_none(
        self,
    ) -> None:
        """Non-assert lines return None."""
        cleanup = AssertCleanup()
        result = cleanup._transform_assert_line("    x = 1")
        assert result is None

    def test_first_top_level_arg_with_nested_parens(self) -> None:
        """Extracts first arg when nested parens are present."""
        cleanup = AssertCleanup()
        result = cleanup._first_top_level_arg("func(a, b), expected")
        assert "func(a, b)" == result

    def test_first_top_level_arg_simple(self) -> None:
        """Extracts first arg from simple comma-separated string."""
        cleanup = AssertCleanup()
        result = cleanup._first_top_level_arg("a, b")
        assert "a" == result

    def test_first_top_level_arg_no_comma(self) -> None:
        """Returns entire string when no top-level comma exists."""
        cleanup = AssertCleanup()
        result = cleanup._first_top_level_arg("func(a, b)")
        assert "func(a, b)" == result

    def test_first_top_level_arg_nested_brackets(self) -> None:
        """Handles nested brackets and braces correctly."""
        cleanup = AssertCleanup()
        result = cleanup._first_top_level_arg("[1, 2], {3: 4}")
        assert "[1, 2]" == result


class TestCleanConcolicTests:
    """clean_concolic_tests test code cleaning."""

    def test_parseable_replaces_assert_comparison(self) -> None:
        """Replaces assert func() == value with func() in parseable code."""
        code = textwrap.dedent("""\
            def test_example():
                assert func() == 42
        """)
        result = clean_concolic_tests(code)
        assert "assert" not in result
        assert "func()" in result

    def test_parseable_leaves_non_assert_unchanged(self) -> None:
        """Non-assert statements are left unchanged in parseable code."""
        code = textwrap.dedent("""\
            def test_example():
                x = func()
                assert x == 42
        """)
        result = clean_concolic_tests(code)
        assert "x = func()" in result

    def test_parseable_leaves_non_compare_assert(self) -> None:
        """Non-Compare asserts (e.g., assert expr) are left unchanged."""
        code = textwrap.dedent("""\
            def test_example():
                assert func()
        """)
        result = clean_concolic_tests(code)
        assert "func()" in result

    def test_unparseable_falls_back_to_regex(self) -> None:
        """Unparseable code falls back to AssertCleanup regex."""
        code = "def test_bad(\n    assert func() == 42\n"
        result = clean_concolic_tests(code)
        assert "func()" in result


class TestMakeEnvWithProjectRoot:
    """make_env_with_project_root PYTHONPATH construction."""

    def test_adds_to_empty_pythonpath(self) -> None:
        """Adds project_root when PYTHONPATH is not set."""
        with patch.dict(os.environ, {}, clear=True):
            env = make_env_with_project_root("/my/project")
            assert "/my/project" in env.get("PYTHONPATH", "")

    def test_prepends_to_existing_pythonpath(self) -> None:
        """Prepends project_root to existing PYTHONPATH."""
        with patch.dict(
            os.environ,
            {"PYTHONPATH": "/existing/path"},
            clear=True,
        ):
            env = make_env_with_project_root("/my/project")
            pythonpath = env["PYTHONPATH"]
            assert pythonpath.startswith("/my/project")
            assert "/existing/path" in pythonpath

    def test_returns_copy_not_original(self) -> None:
        """Returns a copy of os.environ, not the original."""
        original = os.environ.copy()
        env = make_env_with_project_root("/my/project")
        assert env is not os.environ
        assert os.environ.get("PYTHONPATH") == original.get("PYTHONPATH")

    def test_accepts_path_object(self) -> None:
        """Accepts a Path object as well as a string."""
        env = make_env_with_project_root(Path("/my/project"))
        assert "/my/project" in env.get("PYTHONPATH", "")

    def test_preserves_other_env_vars(self) -> None:
        """Other environment variables are preserved in the copy."""
        with patch.dict(
            os.environ,
            {"MY_VAR": "my_value"},
            clear=True,
        ):
            env = make_env_with_project_root("/my/project")
            assert "my_value" == env.get("MY_VAR")
