"""Tests for all newly implemented features from Plan 52.

Tests:
1. MCP Server expansion (4 new tools)
2. No-Code Quarantine (pytest plugin + CLI)
3. GitHub Action YAML validation
4. Auto-Fix PR Suggestions
5. AI Test Generation
6. Branch-Aware Flaky Tracking
7. Local Variable Capture
"""

# =============================================================================
# 1. MCP Server Tests
# =============================================================================
# Import local mcp module by adding to path
import importlib.util
import os
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch

import pytest

_mcp_spec = importlib.util.spec_from_file_location(
    "hikaflow_mcp_server",
    os.path.join(os.path.dirname(__file__), "..", "mcp_server", "server.py")
)
_hikaflow_mcp = importlib.util.module_from_spec(_mcp_spec)


class TestMCPServer:
    """Test the expanded MCP server tools."""

    @pytest.fixture(autouse=True)
    def load_mcp_module(self):
        """Load the local MCP module."""
        _mcp_spec.loader.exec_module(_hikaflow_mcp)
        self.mcp = _hikaflow_mcp

    def test_mcp_tools_registered(self):
        """Verify all 9 tools are registered."""
        expected_tools = [
            "list_sessions",
            "get_session_chunks",
            "get_session_links",
            "get_run_slice",
            "get_replay_artifacts",
            "find_flaky_tests",
            "get_divergence_details",
            "search_replays",
            "get_fix_suggestions",
        ]

        for tool in expected_tools:
            assert tool in self.mcp.TOOLS, f"Tool {tool} not found in MCP server"

    def test_find_flaky_tests_handler(self):
        """Test find_flaky_tests handler structure."""
        # Mock the API request
        with patch.object(self.mcp, "_request") as mock_request:
            mock_request.return_value = {
                "tests": [
                    {
                        "test_id": "test_foo.py::test_bar",
                        "flakiness_score": 0.5,
                        "pass_rate": 0.7,
                        "total_runs": 10,
                    }
                ],
                "total_count": 1,
            }

            result = self.mcp._find_flaky_tests({
                "project_id": "proj123",
                "min_score": 0.3,
            })

            assert "flaky_tests" in result
            assert "summary" in result
            assert len(result["flaky_tests"]) == 1

    def test_get_divergence_details_handler(self):
        """Test get_divergence_details requires run_id."""
        with pytest.raises(ValueError, match="run_id is required"):
            self.mcp._get_divergence_details({})

    def test_search_replays_handler(self):
        """Test search_replays handler."""
        with patch.object(self.mcp, "_request") as mock_request:
            mock_request.return_value = {
                "runs": [{"id": "run1", "test_name": "test_foo"}],
                "total_count": 1,
            }

            result = self.mcp._search_replays({"query": "KeyError"})

            assert "runs" in result
            assert "query_summary" in result

    def test_get_fix_suggestions_requires_run_id(self):
        """Test get_fix_suggestions requires run_id."""
        with pytest.raises(ValueError, match="run_id is required"):
            self.mcp._get_fix_suggestions({})


# =============================================================================
# 2. No-Code Quarantine Tests
# =============================================================================

class TestNoCodeQuarantine:
    """Test the no-code quarantine pytest plugin."""

    def test_is_quarantine_enabled_env_var(self):
        """Test quarantine enabled via environment variable."""
        from autodebug.pytest_plugin import _is_quarantine_enabled

        mock_config = MagicMock()
        mock_config.getoption.return_value = False
        mock_config.getini.return_value = False

        # Test env var
        with patch.dict(os.environ, {"HIKAFLOW_QUARANTINE": "1"}):
            assert _is_quarantine_enabled(mock_config) is True

        with patch.dict(os.environ, {"HIKAFLOW_QUARANTINE": "true"}):
            assert _is_quarantine_enabled(mock_config) is True

        with patch.dict(os.environ, {"HIKAFLOW_QUARANTINE": ""}):
            assert _is_quarantine_enabled(mock_config) is False

    def test_is_quarantine_enabled_cli_flag(self):
        """Test quarantine enabled via CLI flag."""
        from autodebug.pytest_plugin import _is_quarantine_enabled

        mock_config = MagicMock()
        mock_config.getoption.return_value = True
        mock_config.getini.return_value = False

        with patch.dict(os.environ, {"HIKAFLOW_QUARANTINE": ""}):
            assert _is_quarantine_enabled(mock_config) is True

    def test_quarantine_manager_cache(self):
        """Test QuarantineManager caches results."""
        from autodebug.flakiness import QuarantineManager

        mgr = QuarantineManager(project_id="test-proj")
        mgr._quarantined_cache = {"test1", "test2"}
        mgr._cache_time = datetime.now(timezone.utc)

        # Should return cached result
        result = mgr.get_quarantined_tests()
        assert result == {"test1", "test2"}


# =============================================================================
# 3. GitHub Action Tests
# =============================================================================

class TestGitHubAction:
    """Test the GitHub Action configuration."""

    # Resolve project root from this test file's location
    _ACTION_PATH = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        "..",
        ".github",
        "actions",
        "hikaflow",
        "action.yml",
    )

    def test_action_yaml_valid(self):
        """Test action.yml is valid YAML."""
        import yaml

        with open(self._ACTION_PATH) as f:
            action = yaml.safe_load(f)

        # Check required fields
        assert "name" in action
        assert "description" in action
        assert "inputs" in action
        assert "outputs" in action
        assert "runs" in action

    def test_action_has_required_inputs(self):
        """Test action has all required inputs."""
        import yaml

        with open(self._ACTION_PATH) as f:
            action = yaml.safe_load(f)

        required_inputs = ["api-token", "project-id"]
        for inp in required_inputs:
            assert inp in action["inputs"], f"Missing required input: {inp}"
            assert action["inputs"][inp].get("required") is True

    def test_action_has_outputs(self):
        """Test action defines expected outputs."""
        import yaml

        with open(self._ACTION_PATH) as f:
            action = yaml.safe_load(f)

        expected_outputs = ["run-id", "status", "flaky-count", "dashboard-url"]
        for out in expected_outputs:
            assert out in action["outputs"], f"Missing output: {out}"


# =============================================================================
# 4. Auto-Fix PR Suggestions Tests
# =============================================================================

class TestAutoFixSuggestions:
    """Test the auto-fix PR suggestion functionality."""

    def test_parse_patch_to_suggestions_single_file(self):
        """Test parsing a simple single-file patch."""
        from api.github_app import parse_patch_to_suggestions

        patch = """--- a/foo.py
+++ b/foo.py
@@ -10,3 +10,4 @@ def bar():
     x = 1
+    y = 2
     return x
"""
        suggestions = parse_patch_to_suggestions(patch)

        assert len(suggestions) == 1
        assert suggestions[0]["path"] == "foo.py"
        assert suggestions[0]["start_line"] == 10

    def test_parse_patch_strips_base_path(self):
        """Test base_path stripping works."""
        from api.github_app import parse_patch_to_suggestions

        patch = """--- a/src/app/foo.py
+++ b/src/app/foo.py
@@ -1,1 +1,2 @@
+# comment
 pass
"""
        suggestions = parse_patch_to_suggestions(patch, base_path="src/app/")

        assert suggestions[0]["path"] == "foo.py"

    def test_create_pr_suggestion_body_format(self):
        """Test create_pr_suggestion builds correct markdown suggestion body."""
        from unittest.mock import patch as mock_patch

        from api.github_app import create_pr_suggestion

        with mock_patch("api.github_app.get_installation_token", return_value="fake-token"), \
             mock_patch("api.github_app._github_api_call") as mock_api_call:
            mock_api_call.return_value = MagicMock(status_code=201, json=lambda: {"id": 1})

            create_pr_suggestion(
                installation_id=123,
                owner="test-owner",
                repo="test-repo",
                pr_number=1,
                commit_sha="abc123",
                path="foo.py",
                start_line=10,
                end_line=10,
                suggestion="    y = 2",
                body="Fix: add missing assignment",
            )

            # Verify the posted body contains the ```suggestion block
            mock_api_call.assert_called_once()
            posted_payload = mock_api_call.call_args.kwargs["json"]
            posted_body = posted_payload["body"]
            assert "```suggestion" in posted_body
            assert "    y = 2" in posted_body
            assert posted_body.endswith("```")


# =============================================================================
# 5. AI Test Generation Tests
# =============================================================================

class TestAITestGeneration:
    """Test the AI test generation from replays."""

    def test_format_http_calls_empty(self):
        """Test formatting empty HTTP calls."""
        from autodebug.agent.test_generator import _format_http_calls

        result = _format_http_calls({})
        assert "No HTTP calls" in result

    def test_format_http_calls_with_data(self):
        """Test formatting HTTP calls with data."""
        from autodebug.agent.test_generator import _format_http_calls

        transcript = {
            "http": [
                {"method": "GET", "url": "https://api.example.com/users", "status": 200},
                {"method": "POST", "url": "https://api.example.com/users", "status": 201},
            ]
        }
        result = _format_http_calls(transcript)

        assert "GET" in result
        assert "POST" in result
        assert "api.example.com" in result

    def test_format_sql_queries_empty(self):
        """Test formatting empty SQL queries."""
        from autodebug.agent.test_generator import _format_sql_queries

        result = _format_sql_queries({})
        assert "No SQL queries" in result

    def test_generate_http_fixture(self):
        """Test HTTP fixture generation."""
        from autodebug.agent.test_generator import generate_http_fixture

        calls = [
            {"method": "GET", "url": "https://api.example.com/test", "status": 200, "response_body": {"ok": True}},
        ]
        fixture = generate_http_fixture(calls, "test_api")

        assert "@pytest.fixture" in fixture
        assert "mock_http_test_api" in fixture
        assert "responses.GET" in fixture

    def test_generated_test_dataclass(self):
        """Test GeneratedTest dataclass."""
        from autodebug.agent.test_generator import GeneratedTest

        test = GeneratedTest(
            name="test_foo",
            code="def test_foo(): pass",
            fixtures=["db"],
            imports=["import pytest"],
            description="Test foo",
            confidence=0.8,
            source_run_id="run123",
        )

        assert test.name == "test_foo"
        assert test.confidence == 0.8

    def test_fallback_test_generation(self):
        """Test fallback test generation when LLM fails."""
        from autodebug.agent.test_generator import _generate_fallback_test

        test = _generate_fallback_test(
            run_id="test123",
            transcript={"http": [{"method": "GET", "url": "http://test.com", "status": 200}]},
            context={"exception_type": "KeyError", "file": "test.py", "line": 10},
        )

        assert test.name.startswith("test_replay_")
        assert "@responses.activate" in test.code
        assert test.confidence == 0.3  # Low confidence for fallback


# =============================================================================
# 6. Branch-Aware Flaky Tracking Tests
# =============================================================================

class TestBranchAwareFlaky:
    """Test branch-aware flaky test tracking."""

    def test_test_result_request_has_branch_fields(self):
        """Test TestResultRequest model has branch fields."""
        # This tests the Pydantic model definition
        from pydantic import BaseModel

        # Check the model accepts branch fields
        class TestResultRequest(BaseModel):
            test_id: str
            passed: bool
            git_branch: str = None
            git_commit: str = None

        req = TestResultRequest(
            test_id="test_foo",
            passed=True,
            git_branch="feature/test",
            git_commit="abc123",
        )

        assert req.git_branch == "feature/test"
        assert req.git_commit == "abc123"

    def test_db_record_test_result_signature(self):
        """Test db.record_test_result accepts branch params."""
        import inspect

        from api.db import record_test_result

        sig = inspect.signature(record_test_result)
        params = list(sig.parameters.keys())

        assert "git_branch" in params
        assert "git_commit" in params

    def test_db_get_test_health_signature(self):
        """Test db.get_test_health accepts branch param."""
        import inspect

        from api.db import get_test_health

        sig = inspect.signature(get_test_health)
        params = list(sig.parameters.keys())

        assert "git_branch" in params


# =============================================================================
# 7. Local Variable Capture Tests
# =============================================================================

class TestLocalVariableCapture:
    """Test local variable capture functionality."""

    def test_safe_repr_basic_types(self):
        """Test safe_repr handles basic types."""
        from autodebug_probe.locals_capture import _safe_repr

        assert _safe_repr(None) == "None"
        assert _safe_repr(True) == "True"
        assert _safe_repr(42) == "42"
        assert _safe_repr(3.14) == "3.14"
        assert _safe_repr("hello") == "'hello'"

    def test_safe_repr_truncates_long_strings(self):
        """Test safe_repr truncates long strings."""
        from autodebug_probe.locals_capture import _safe_repr

        long_string = "x" * 1000
        result = _safe_repr(long_string)

        assert len(result) < 1000
        assert "chars" in result  # Should mention total length

    def test_safe_repr_handles_collections(self):
        """Test safe_repr handles lists and dicts."""
        from autodebug_probe.locals_capture import _safe_repr

        assert "[1, 2, 3]" == _safe_repr([1, 2, 3])
        assert "(1, 2)" == _safe_repr((1, 2))

    def test_is_pii_key_detection(self):
        """Test PII key detection."""
        from autodebug_probe.locals_capture import _is_pii_key

        assert _is_pii_key("password") is True
        assert _is_pii_key("user_password") is True
        assert _is_pii_key("api_key") is True
        assert _is_pii_key("secret_token") is True
        assert _is_pii_key("username") is False
        assert _is_pii_key("data") is False

    def test_safe_repr_redacts_pii_in_dicts(self):
        """Test safe_repr redacts PII values in dicts."""
        from autodebug_probe.locals_capture import _safe_repr

        data = {"password": "secret123", "username": "john"}
        result = _safe_repr(data)

        assert "<redacted>" in result
        assert "secret123" not in result
        assert "john" in result

    def test_capture_frame_locals(self):
        """Test capturing locals from a frame."""
        from autodebug_probe.locals_capture import capture_frame_locals

        # Create a test function to capture from
        def test_func():
            x = 42
            y = "hello"
            try:
                raise ValueError("test")
            except:
                import sys
                return sys.exc_info()[2].tb_frame

        frame = test_func()
        result = capture_frame_locals(frame, 1)

        assert result.function == "test_func"
        var_names = [v.name for v in result.variables]
        assert "x" in var_names
        assert "y" in var_names

    def test_capture_exception_locals(self):
        """Test capturing locals from an exception."""
        from autodebug_probe.locals_capture import capture_exception_locals

        try:
            x = 42
            y = "test"
            raise ValueError("test error")
        except ValueError:
            import sys
            exc_type, exc_value, exc_tb = sys.exc_info()
            snapshot = capture_exception_locals(exc_type, exc_value, exc_tb)

        assert snapshot.exception_type == "ValueError"
        assert "test error" in snapshot.exception_message
        assert len(snapshot.frames) > 0

    def test_snapshot_to_dict(self):
        """Test converting snapshot to dict."""
        from autodebug_probe.locals_capture import (
            FrameLocals,
            LocalsSnapshot,
            LocalVariable,
            snapshot_to_dict,
        )

        snapshot = LocalsSnapshot(
            exception_type="ValueError",
            exception_message="test",
            frames=[
                FrameLocals(
                    filename="test.py",
                    lineno=10,
                    function="test_func",
                    variables=[
                        LocalVariable(name="x", value="42", type_name="int"),
                    ],
                )
            ],
        )

        result = snapshot_to_dict(snapshot)

        assert result["exception_type"] == "ValueError"
        assert len(result["frames"]) == 1
        assert result["frames"][0]["variables"][0]["name"] == "x"

    def test_capture_on_exception_decorator(self):
        """Test the capture_on_exception decorator."""
        from autodebug_probe.locals_capture import (
            capture_on_exception,
            clear_captured_snapshots,
            get_captured_snapshots,
        )

        clear_captured_snapshots()

        @capture_on_exception
        def failing_func():
            x = 123
            raise RuntimeError("deliberate failure")

        with pytest.raises(RuntimeError):
            failing_func()

        snapshots = get_captured_snapshots()
        assert len(snapshots) == 1
        assert snapshots[0].exception_type == "RuntimeError"

        clear_captured_snapshots()

    def test_capture_locals_context_manager(self):
        """Test the CaptureLocalsContext context manager."""
        from autodebug_probe.locals_capture import CaptureLocalsContext

        with CaptureLocalsContext() as ctx:
            x = 42
            # No exception, so no snapshot
            pass

        assert ctx.snapshot is None

        with pytest.raises(KeyError):
            with CaptureLocalsContext() as ctx:
                data = {}
                value = data["missing"]

        assert ctx.snapshot is not None
        assert ctx.snapshot.exception_type == "KeyError"


# =============================================================================
# Integration Tests
# =============================================================================

class TestIntegration:
    """Integration tests across features."""

    def test_mcp_and_db_integration(self):
        """Test MCP server can call DB functions."""
        # The MCP server uses _request which calls the API
        # This tests the function signatures match
        import inspect

        from api.db import list_flaky_tests, search_runs

        # Check db functions exist and have expected params
        db_flaky_sig = inspect.signature(list_flaky_tests)
        assert "org_id" in db_flaky_sig.parameters
        assert "min_score" in db_flaky_sig.parameters

        db_search_sig = inspect.signature(search_runs)
        assert "org_id" in db_search_sig.parameters
        assert "query" in db_search_sig.parameters

        # Also check MCP server has matching handlers
        _mcp_spec.loader.exec_module(_hikaflow_mcp)
        assert "find_flaky_tests" in _hikaflow_mcp.TOOLS
        assert "search_replays" in _hikaflow_mcp.TOOLS

    def test_quarantine_and_flaky_integration(self):
        """Test quarantine works with flaky detection."""
        from autodebug.flakiness import (
            FlakinessCalculator,
            TestResultRecord,
        )

        # Create test results that show flakiness
        calculator = FlakinessCalculator(window=5)
        results = [
            TestResultRecord(test_id="test_flaky", passed=True),
            TestResultRecord(test_id="test_flaky", passed=False),
            TestResultRecord(test_id="test_flaky", passed=True),
            TestResultRecord(test_id="test_flaky", passed=False),
            TestResultRecord(test_id="test_flaky", passed=True),
        ]

        health = calculator.calculate_from_results(results)

        # 4 transitions in 5 runs = highly flaky
        assert health.flakiness_score > 0.5
        assert health.is_flaky is True


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
