"""Tests for LintGate quality gate."""

import subprocess
from unittest.mock import MagicMock, patch

from src.gates.lint_gate import LintGate
from src.gates.quality_gate import GateResult


class TestLintGate:
    """Test suite for LintGate."""

    def test_check_success(self) -> None:
        """Test that check() returns passed=True when ruff finds no issues."""
        # Mock subprocess.run to simulate successful ruff run
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "All checks passed!"
        mock_result.stderr = ""

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            gate = LintGate()
            result = gate.check()

            # Verify subprocess.run was called with correct arguments
            mock_run.assert_called_once()
            call_args = mock_run.call_args
            assert "ruff" in call_args[0][0]
            assert "check" in call_args[0][0]
            assert "src/" 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["return_code"] == 0

    def test_check_failure_lint_errors(self) -> None:
        """Test that check() returns passed=False when ruff finds errors."""
        # Mock subprocess.run to simulate ruff finding errors
        mock_result = MagicMock()
        mock_result.returncode = 1
        mock_result.stdout = (
            "src/main.py:10:1: F401 'os' imported but unused\n"
            "src/models.py:5:1: E501 Line too long (105 > 100 characters)\n"
            "Found 2 errors."
        )
        mock_result.stderr = ""

        with patch("subprocess.run", return_value=mock_result):
            gate = LintGate()
            result = gate.check()

            # Verify result
            assert isinstance(result, GateResult)
            assert result.passed is False
            assert "failed" in result.message.lower() or "error" in result.message.lower()
            assert result.details["return_code"] == 1
            assert "stdout" in result.details
            assert "2 errors" in result.details["stdout"]

    def test_check_treats_warnings_as_failures(self) -> None:
        """Test that check() treats warnings as failures (non-negotiable requirement)."""
        # Mock subprocess.run to simulate ruff finding warnings
        # Note: ruff doesn't have separate warning levels, but this tests the principle
        mock_result = MagicMock()
        mock_result.returncode = 1  # Any non-zero is failure
        mock_result.stdout = "src/main.py:15:1: W505 Doc line too long"
        mock_result.stderr = ""

        with patch("subprocess.run", return_value=mock_result):
            gate = LintGate()
            result = gate.check()

            # Verify result
            assert isinstance(result, GateResult)
            assert result.passed is False
            assert "failed" in result.message.lower() or "error" 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, "ruff")
        ):
            gate = LintGate()
            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 ruff is not installed."""
        # Mock subprocess.run to raise FileNotFoundError
        with patch("subprocess.run", side_effect=FileNotFoundError("ruff not found")):
            gate = LintGate()
            result = gate.check()

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

    def test_check_uses_select_flags(self) -> None:
        """Test that check() runs ruff with configured linting rules."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "All checks passed!"
        mock_result.stderr = ""

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            gate = LintGate()
            gate.check()

            # Verify ruff check is called
            call_args = mock_run.call_args[0][0]
            assert isinstance(call_args, list)
            assert "ruff" in call_args
            assert "check" in call_args

    def test_check_captures_output(self) -> None:
        """Test that check() captures both stdout and stderr."""
        mock_result = MagicMock()
        mock_result.returncode = 1
        mock_result.stdout = "Some lint errors"
        mock_result.stderr = "Some warnings"

        with patch("subprocess.run", return_value=mock_result):
            gate = LintGate()
            result = gate.check()

            # Verify both stdout and stderr are captured
            assert "stdout" in result.details or "stderr" in result.details
            assert result.details["return_code"] == 1

    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 = LintGate()
            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_check_uses_timeout(self) -> None:
        """Test that check() sets a timeout on subprocess.run."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "All checks passed!"
        mock_result.stderr = ""

        with patch("subprocess.run", return_value=mock_result) as mock_run:
            gate = LintGate()
            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("ruff", timeout=300),
        ):
            gate = LintGate()
            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
