"""Tests for CoverageGate quality gate."""

import json
import subprocess
from unittest.mock import MagicMock, mock_open, patch

from src.gates.coverage_gate import CoverageGate
from src.gates.quality_gate import GateResult


class TestCoverageGate:
    """Test suite for CoverageGate."""

    def test_check_success_meets_minimum_coverage(self) -> None:
        """Test that check() returns passed=True when coverage meets 85% minimum."""
        # Mock subprocess.run to simulate successful coverage run
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = (
            "============================= test session starts ==============================\n"
            "collected 50 items\n"
            "tests/test_example.py .................................................. [100%]\n"
            "---------- coverage: platform linux, python 3.11 -----------\n"
            "Name                   Stmts   Miss  Cover\n"
            "------------------------------------------\n"
            "src/main.py               100     10    90%\n"
            "src/models.py              50      5    90%\n"
            "------------------------------------------\n"
            "TOTAL                     150     15    90%\n"
            "============================== 50 passed in 2.34s ===============================\n"
        )
        mock_result.stderr = ""

        # Mock .coverage file reading
        coverage_data = {
            "totals": {"percent_covered": 90.0, "covered_lines": 135, "missing_lines": 15}
        }

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            with patch("builtins.open", mock_open(read_data=json.dumps(coverage_data))):
                with patch("json.load", return_value=coverage_data):
                    gate = CoverageGate()
                    result = gate.check()

                    # Verify subprocess.run was called with correct arguments
                    mock_run.assert_called_once()
                    call_args = mock_run.call_args
                    assert "pytest" in call_args[0][0] or "python" in call_args[0][0]
                    # Should include --cov flag
                    assert any("--cov" in str(arg) for arg in call_args[0][0])

                    # Verify result
                    assert isinstance(result, GateResult)
                    assert result.passed is True
                    assert "passed" in result.message.lower()
                    assert result.details["coverage_percent"] >= 85.0

    def test_check_success_exactly_85_percent(self) -> None:
        """Test that check() passes when coverage is exactly 85% (boundary test)."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "TOTAL     100     15    85%"
        mock_result.stderr = ""

        coverage_data = {"totals": {"percent_covered": 85.0}}

        with patch("subprocess.run", return_value=mock_result):
            with patch("builtins.open", mock_open(read_data=json.dumps(coverage_data))):
                with patch("json.load", return_value=coverage_data):
                    gate = CoverageGate()
                    result = gate.check()

                    # Verify result - exactly 85% should pass
                    assert isinstance(result, GateResult)
                    assert result.passed is True
                    assert result.details["coverage_percent"] == 85.0

    def test_check_failure_below_minimum_coverage(self) -> None:
        """Test that check() returns passed=False when coverage is below 85%."""
        mock_result = MagicMock()
        mock_result.returncode = 1  # pytest-cov returns 1 when below threshold
        mock_result.stdout = (
            "TOTAL     100     20    80%\n"
            "FAIL Required test coverage of 85% not reached. Total coverage: 80.00%"
        )
        mock_result.stderr = ""

        coverage_data = {"totals": {"percent_covered": 80.0}}

        with patch("subprocess.run", return_value=mock_result):
            with patch("builtins.open", mock_open(read_data=json.dumps(coverage_data))):
                with patch("json.load", return_value=coverage_data):
                    gate = CoverageGate()
                    result = gate.check()

                    # Verify result
                    assert isinstance(result, GateResult)
                    assert result.passed is False
                    assert (
                        "below minimum" in result.message.lower()
                        or "failed" in result.message.lower()
                    )
                    assert result.details["coverage_percent"] < 85.0
                    assert result.details["minimum_coverage"] == 85.0

    def test_check_failure_84_percent(self) -> None:
        """Test that check() fails when coverage is 84% (just below threshold)."""
        mock_result = MagicMock()
        mock_result.returncode = 1
        mock_result.stdout = "TOTAL     100     16    84%"
        mock_result.stderr = ""

        coverage_data = {"totals": {"percent_covered": 84.0}}

        with patch("subprocess.run", return_value=mock_result):
            with patch("builtins.open", mock_open(read_data=json.dumps(coverage_data))):
                with patch("json.load", return_value=coverage_data):
                    gate = CoverageGate()
                    result = gate.check()

                    # Verify result - 84% should fail
                    assert isinstance(result, GateResult)
                    assert result.passed is False
                    assert result.details["coverage_percent"] == 84.0

    def test_check_failure_no_coverage_data(self) -> None:
        """Test that check() fails when no coverage data is available."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "No coverage data"
        mock_result.stderr = ""

        # Mock file not found when trying to read .coverage
        with patch("subprocess.run", return_value=mock_result):
            with patch("builtins.open", side_effect=FileNotFoundError(".coverage not found")):
                gate = CoverageGate()
                result = gate.check()

                # Verify result
                assert isinstance(result, GateResult)
                assert result.passed is False
                assert (
                    "no coverage data" in result.message.lower()
                    or "not found" in result.message.lower()
                )

    def test_check_failure_subprocess_error(self) -> None:
        """Test that check() handles subprocess errors gracefully."""
        # Mock subprocess.run to raise CalledProcessError
        with patch(
            "subprocess.run", side_effect=subprocess.CalledProcessError(127, "pytest")
        ):
            gate = CoverageGate()
            result = gate.check()

            # Verify result
            assert isinstance(result, GateResult)
            assert result.passed is False
            assert "error" in result.message.lower()
            assert "error" in result.details

    def test_check_failure_file_not_found(self) -> None:
        """Test that check() handles FileNotFoundError when pytest is not installed."""
        # Mock subprocess.run to raise FileNotFoundError
        with patch("subprocess.run", side_effect=FileNotFoundError("pytest not found")):
            gate = CoverageGate()
            result = gate.check()

            # Verify result
            assert isinstance(result, GateResult)
            assert result.passed is False
            assert "pytest" in result.message.lower() or "not found" in result.message.lower()
            assert "error" in result.details

    def test_check_enforces_85_percent_minimum(self) -> None:
        """Test that check() enforces exactly 85% minimum (non-negotiable requirement)."""
        gate = CoverageGate()
        # Verify the minimum coverage constant
        assert gate.MINIMUM_COVERAGE == 85.0

    def test_check_includes_coverage_details(self) -> None:
        """Test that check() includes coverage details in result."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "TOTAL     100     10    90%"
        mock_result.stderr = ""

        coverage_data = {
            "totals": {
                "percent_covered": 90.0,
                "covered_lines": 90,
                "missing_lines": 10,
                "num_statements": 100,
            }
        }

        with patch("subprocess.run", return_value=mock_result):
            with patch("builtins.open", mock_open(read_data=json.dumps(coverage_data))):
                with patch("json.load", return_value=coverage_data):
                    gate = CoverageGate()
                    result = gate.check()

                    # Verify coverage details are included
                    assert "coverage_percent" in result.details
                    assert "minimum_coverage" in result.details
                    assert result.details["minimum_coverage"] == 85.0

    def test_check_handles_unexpected_exception(self) -> None:
        """Test that check() handles unexpected exceptions gracefully."""
        # Mock subprocess.run to raise a generic exception
        with patch("subprocess.run", side_effect=RuntimeError("Unexpected error")):
            gate = CoverageGate()
            result = gate.check()

            # Verify result
            assert isinstance(result, GateResult)
            assert result.passed is False
            assert "unexpected error" in result.message.lower()
            assert "error" in result.details

    def test_extract_coverage_from_json_with_invalid_json(self) -> None:
        """Test that _extract_coverage_from_json handles invalid JSON gracefully."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "TOTAL     100     10    90%"
        mock_result.stderr = ""

        # Mock json.load to raise JSONDecodeError
        with patch("subprocess.run", return_value=mock_result):
            with patch("builtins.open", mock_open(read_data="{invalid json")):
                with patch("json.load", side_effect=json.JSONDecodeError("error", "", 0)):
                    gate = CoverageGate()
                    result = gate.check()

                    # Should fallback to parsing stdout
                    assert isinstance(result, GateResult)
                    assert result.passed is True
                    assert result.details["coverage_percent"] == 90.0

    def test_extract_coverage_from_output_with_invalid_percentage(self) -> None:
        """Test that _extract_coverage_from_output handles invalid percentage gracefully."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        # Include a TOTAL line with invalid percentage
        mock_result.stdout = "TOTAL     100     10    invalid%\nTOTAL     100     10    90%"
        mock_result.stderr = ""

        with patch("subprocess.run", return_value=mock_result):
            with patch("builtins.open", side_effect=FileNotFoundError()):
                gate = CoverageGate()
                result = gate.check()

                # Should skip invalid percentage and find valid one
                assert isinstance(result, GateResult)
                assert result.passed is True
                assert result.details["coverage_percent"] == 90.0

    def test_check_uses_timeout(self) -> None:
        """Test that check() sets a timeout on subprocess.run."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "TOTAL     100     10    90%"
        mock_result.stderr = ""

        coverage_data = {"totals": {"percent_covered": 90.0}}

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            with patch("builtins.open", mock_open(read_data=json.dumps(coverage_data))):
                with patch("json.load", return_value=coverage_data):
                    gate = CoverageGate()
                    gate.check()

                    # Verify timeout is set
                    mock_run.assert_called_once()
                    call_kwargs = mock_run.call_args[1]
                    assert "timeout" in call_kwargs
                    assert call_kwargs["timeout"] == 300  # 5 minutes

    def test_check_handles_timeout_exception(self) -> None:
        """Test that check() handles subprocess timeout gracefully."""
        # Mock subprocess.run to raise TimeoutExpired
        with patch(
            "subprocess.run",
            side_effect=subprocess.TimeoutExpired("pytest", timeout=300),
        ):
            gate = CoverageGate()
            result = gate.check()

            # Verify result
            assert isinstance(result, GateResult)
            assert result.passed is False
            # TimeoutExpired message contains "timed out after"
            assert "timed out" in result.message.lower() or "timeout" in result.message.lower()
            assert "error" in result.details

    def test_coverage_file_path_is_absolute(self) -> None:
        """Test that coverage.json path is resolved as absolute and validated."""
        from pathlib import Path

        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "TOTAL     100     10    90%"
        mock_result.stderr = ""

        coverage_data = {"totals": {"percent_covered": 90.0}}

        with patch("subprocess.run", return_value=mock_result):
            # Mock Path.exists to return True for absolute path check
            with patch.object(Path, "exists", return_value=True):
                with patch("builtins.open", mock_open(read_data=json.dumps(coverage_data))):
                    with patch("json.load", return_value=coverage_data):
                        gate = CoverageGate()
                        # Access the internal method to verify it uses absolute paths
                        coverage_percent = gate._extract_coverage_from_json()

                        # Should successfully extract coverage
                        assert coverage_percent == 90.0
