"""Tests for Leanstral formal verification module.

Tests the full pipeline: auto-formalization → proof generation → kernel verification.
All Mistral API calls are mocked. Lean kernel calls are mocked unless testing
kernel integration specifically.

Constitutional Hash: 608508a9bd224290
"""

from __future__ import annotations

import json
from typing import Any
from unittest.mock import MagicMock, patch, call

import pytest

from acgs_lite.lean_verify import (
    LEAN_AVAILABLE,
    LeanstralVerifier,
    LeanVerifyResult,
    ProofCertificate,
    _build_formalization_prompt,
    _build_proof_prompt,
    _lean_escape,
    _lean_safe_name,
    _parse_json_response,
    _run_lean_check,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


def _make_rules(n: int = 2) -> list[dict[str, str]]:
    """Create n governance rules for testing."""
    base = [
        {"id": "MACI-1", "text": "No agent may validate its own proposals"},
        {"id": "MACI-2", "text": "Role promotion requires judicial approval"},
        {"id": "MACI-3", "text": "All actions must be audited"},
        {"id": "MACI-4", "text": "Executive cannot act as validator"},
    ]
    return base[:n]


def _make_formalization_response() -> str:
    """Mock response from Leanstral's auto-formalization step."""
    return json.dumps(
        {
            "prelude": (
                "structure ActionCtx where\n"
                "  agentRole : String\n"
                "  targetRole : String\n"
                "  action : String"
            ),
            "predicates": {
                "MACI-1": (
                    "def maci_1_satisfied (ctx : ActionCtx) : Prop :=\n"
                    '  ctx.agentRole ≠ "validator" ∨ ctx.action ≠ "validate_own"'
                ),
                "MACI-2": (
                    "def maci_2_satisfied (ctx : ActionCtx) : Prop :=\n"
                    '  ctx.action ≠ "promote" ∨ ctx.agentRole = "judicial"'
                ),
            },
        }
    )


def _make_proof_response() -> str:
    """Mock response from Leanstral's proof generation step."""
    return "by\n  simp [maci_1_satisfied, maci_2_satisfied]\n  tauto"


def _mock_chat_responses(*responses: str) -> MagicMock:
    """Build a mock Mistral client that returns a sequence of responses."""
    client = MagicMock()
    side_effects = []
    for resp_text in responses:
        choice = MagicMock()
        choice.message.content = resp_text
        response = MagicMock()
        response.choices = [choice]
        response.model = "leanstral"
        side_effects.append(response)
    client.chat.complete.side_effect = side_effects
    return client


# ---------------------------------------------------------------------------
# ProofCertificate
# ---------------------------------------------------------------------------


class TestProofCertificate:
    def test_fields(self) -> None:
        cert = ProofCertificate(
            lean_statement="theorem t : True",
            lean_proof="by trivial",
            kernel_verified=True,
            rules_formalized={"R1": "def r1 : Prop := True"},
            proof_hash="abc123",
            model_used="leanstral",
            verification_time_ms=42.0,
        )
        assert cert.kernel_verified is True
        assert cert.proof_hash == "abc123"

    def test_to_audit_dict(self) -> None:
        cert = ProofCertificate(
            lean_statement="theorem t : True",
            lean_proof="by trivial",
            kernel_verified=False,
            rules_formalized={"R1": "def r1 : Prop := True"},
            proof_hash="abc123",
            model_used="leanstral",
            verification_time_ms=42.0,
        )
        d = cert.to_audit_dict()
        assert d["type"] == "lean4_proof_certificate"
        assert d["kernel_verified"] is False
        assert "lean_statement" in d
        assert "proof_hash" in d

    def test_immutable(self) -> None:
        cert = ProofCertificate(
            lean_statement="t",
            lean_proof="p",
            kernel_verified=True,
            rules_formalized={},
            proof_hash="h",
            model_used="m",
            verification_time_ms=0.0,
        )
        with pytest.raises(AttributeError):
            cert.kernel_verified = False  # type: ignore[misc]


# ---------------------------------------------------------------------------
# LeanVerifyResult
# ---------------------------------------------------------------------------


class TestLeanVerifyResult:
    def test_result_fields(self) -> None:
        result = LeanVerifyResult(
            proved=True,
            verified=True,
            certificate=None,
            counterexample=None,
            lean_errors=[],
            attempts=1,
            model_used="leanstral",
            verification_time_ms=42.0,
        )
        assert result.proved is True
        assert result.error is None

    def test_result_immutable(self) -> None:
        result = LeanVerifyResult(
            proved=False,
            verified=False,
            certificate=None,
            counterexample="MACI-1 violated",
            lean_errors=[],
            attempts=0,
            model_used="leanstral",
            verification_time_ms=0.0,
            error="test",
        )
        with pytest.raises(AttributeError):
            result.proved = True  # type: ignore[misc]


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


class TestHelpers:
    def test_lean_safe_name(self) -> None:
        assert _lean_safe_name("MACI-1") == "maci_1"
        assert _lean_safe_name("Rule With Spaces") == "rule_with_spaces"

    def test_lean_escape(self) -> None:
        assert _lean_escape('hello "world"') == 'hello \\"world\\"'
        assert _lean_escape("line\nbreak") == "line\\nbreak"
        assert _lean_escape("back\\slash") == "back\\\\slash"


# ---------------------------------------------------------------------------
# Prompt building
# ---------------------------------------------------------------------------


class TestBuildFormalizationPrompt:
    def test_includes_rules(self) -> None:
        prompt = _build_formalization_prompt(_make_rules(2))
        assert "MACI-1" in prompt
        assert "MACI-2" in prompt
        assert "No agent may validate" in prompt

    def test_includes_context(self) -> None:
        prompt = _build_formalization_prompt(_make_rules(1), context={"agent_role": "executive"})
        assert "agent_role" in prompt
        assert "executive" in prompt


class TestBuildProofPrompt:
    def test_includes_source_and_theorem(self) -> None:
        prompt = _build_proof_prompt("test action", "-- source", "theorem t : True")
        assert "-- source" in prompt
        assert "theorem t : True" in prompt

    def test_includes_previous_errors(self) -> None:
        prompt = _build_proof_prompt(
            "test",
            "-- src",
            "theorem t",
            previous_errors=["error: type mismatch"],
        )
        assert "type mismatch" in prompt
        assert "Fix the proof" in prompt

    def test_no_errors_on_first_attempt(self) -> None:
        prompt = _build_proof_prompt("test", "-- src", "theorem t")
        assert "Previous attempt" not in prompt


# ---------------------------------------------------------------------------
# Response parsing
# ---------------------------------------------------------------------------


class TestParseJsonResponse:
    def test_valid_json(self) -> None:
        raw = json.dumps({"prelude": "test", "predicates": {"R1": "def r1"}})
        result = _parse_json_response(raw)
        assert result["prelude"] == "test"

    def test_json_in_code_fence(self) -> None:
        raw = '```json\n{"prelude": "test"}\n```'
        result = _parse_json_response(raw)
        assert result["prelude"] == "test"

    def test_empty(self) -> None:
        assert _parse_json_response(None) == {}
        assert _parse_json_response("") == {}

    def test_invalid_json(self) -> None:
        assert _parse_json_response("not json at all") == {}


# ---------------------------------------------------------------------------
# Lean kernel
# ---------------------------------------------------------------------------


class TestRunLeanCheck:
    @patch("acgs_lite.lean_verify.LEAN_AVAILABLE", False)
    def test_lean_not_installed(self) -> None:
        ok, errors = _run_lean_check("theorem t : True := by trivial")
        assert ok is False
        assert "not installed" in errors[0]

    @patch("acgs_lite.lean_verify.LEAN_AVAILABLE", True)
    @patch("acgs_lite.lean_verify.subprocess.run")
    def test_lean_success(self, mock_run: MagicMock) -> None:
        mock_run.return_value = MagicMock(returncode=0, stderr="")
        ok, errors = _run_lean_check("theorem t : True := by trivial")
        assert ok is True
        assert errors == []

    @patch("acgs_lite.lean_verify.LEAN_AVAILABLE", True)
    @patch("acgs_lite.lean_verify.subprocess.run")
    def test_lean_failure(self, mock_run: MagicMock) -> None:
        mock_run.return_value = MagicMock(
            returncode=1,
            stderr="error: type mismatch\n  expected: Nat\n  got: String",
        )
        ok, errors = _run_lean_check("bad source")
        assert ok is False
        assert any("type mismatch" in e for e in errors)

    @patch("acgs_lite.lean_verify.LEAN_AVAILABLE", True)
    @patch("acgs_lite.lean_verify.subprocess.run")
    def test_lean_timeout(self, mock_run: MagicMock) -> None:
        import subprocess

        mock_run.side_effect = subprocess.TimeoutExpired("lean", 30)
        ok, errors = _run_lean_check("slow source", timeout_s=30)
        assert ok is False
        assert "timed out" in errors[0]


# ---------------------------------------------------------------------------
# Verifier — unavailable
# ---------------------------------------------------------------------------


class TestLeanstralVerifierUnavailable:
    @patch("acgs_lite.lean_verify.MISTRAL_AVAILABLE", False)
    def test_not_installed(self) -> None:
        verifier = LeanstralVerifier()
        assert verifier.available is False
        result = verifier.verify("test", _make_rules(1))
        assert result.verified is False
        assert result.proved is False
        assert "not installed" in (result.error or "")

    @patch("acgs_lite.lean_verify.MISTRAL_AVAILABLE", True)
    def test_no_api_key(self) -> None:
        verifier = LeanstralVerifier(api_key=None)
        with patch.dict("os.environ", {}, clear=True):
            result = verifier.verify("test", _make_rules(1))
            assert result.verified is False
            assert "MISTRAL_API_KEY" in (result.error or "")


# ---------------------------------------------------------------------------
# Verifier — mocked pipeline (no Lean kernel)
# ---------------------------------------------------------------------------


@patch("acgs_lite.lean_verify.MISTRAL_AVAILABLE", True)
@patch("acgs_lite.lean_verify.LEAN_AVAILABLE", False)
class TestLeanstralVerifierMockedNoKernel:
    """Tests with Leanstral mocked and no Lean kernel."""

    def _make_verifier(self, mock_client: MagicMock) -> LeanstralVerifier:
        verifier = LeanstralVerifier(api_key="test-key")
        verifier._client = mock_client
        return verifier

    def test_full_pipeline_proves(self) -> None:
        client = _mock_chat_responses(
            _make_formalization_response(),
            _make_proof_response(),
        )
        verifier = self._make_verifier(client)

        result = verifier.verify(
            "read audit log",
            _make_rules(2),
            context={"agent_role": "judicial"},
        )

        assert result.proved is True
        assert result.verified is True
        assert result.certificate is not None
        assert result.certificate.kernel_verified is False  # No Lean installed
        assert result.certificate.lean_proof is not None
        assert result.certificate.proof_hash  # Non-empty hash
        assert result.attempts == 1
        assert result.verification_time_ms > 0
        assert client.chat.complete.call_count == 2  # formalize + prove

    def test_certificate_audit_dict(self) -> None:
        client = _mock_chat_responses(
            _make_formalization_response(),
            _make_proof_response(),
        )
        verifier = self._make_verifier(client)
        result = verifier.verify("test", _make_rules(2))

        assert result.certificate is not None
        d = result.certificate.to_audit_dict()
        assert d["type"] == "lean4_proof_certificate"
        assert d["kernel_verified"] is False
        assert len(d["rules_formalized"]) == 2

    def test_formalization_failure(self) -> None:
        client = _mock_chat_responses(
            json.dumps({"prelude": "", "predicates": {}}),  # Empty predicates
        )
        verifier = self._make_verifier(client)
        result = verifier.verify("test", _make_rules(1))

        assert result.proved is False
        assert result.verified is True
        assert "Failed to formalize" in (result.counterexample or "")

    def test_api_error(self) -> None:
        client = MagicMock()
        client.chat.complete.side_effect = RuntimeError("API down")
        verifier = self._make_verifier(client)

        result = verifier.verify("test", _make_rules(1))
        assert result.proved is False
        assert result.verified is False
        assert "RuntimeError" in (result.error or "")

    def test_model_selection(self) -> None:
        client = _mock_chat_responses(
            _make_formalization_response(),
            _make_proof_response(),
        )
        verifier = LeanstralVerifier(api_key="test-key", model="codestral-latest")
        verifier._client = client

        result = verifier.verify("test", _make_rules(2))
        # Both calls should use the specified model
        for c in client.chat.complete.call_args_list:
            assert c.kwargs["model"] == "codestral-latest"

    def test_temperature_zero(self) -> None:
        client = _mock_chat_responses(
            _make_formalization_response(),
            _make_proof_response(),
        )
        verifier = self._make_verifier(client)
        verifier.verify("test", _make_rules(2))

        for c in client.chat.complete.call_args_list:
            assert c.kwargs["temperature"] == 0.0

    def test_multiple_rules_in_theorem(self) -> None:
        client = _mock_chat_responses(
            _make_formalization_response(),
            _make_proof_response(),
        )
        verifier = self._make_verifier(client)
        result = verifier.verify("test", _make_rules(2))

        assert result.certificate is not None
        # Theorem should conjoin both rules
        assert "maci_1_satisfied" in result.certificate.lean_statement
        assert "maci_2_satisfied" in result.certificate.lean_statement
        assert "∧" in result.certificate.lean_statement


# ---------------------------------------------------------------------------
# Verifier — mocked pipeline WITH Lean kernel
# ---------------------------------------------------------------------------


@patch("acgs_lite.lean_verify.MISTRAL_AVAILABLE", True)
@patch("acgs_lite.lean_verify.LEAN_AVAILABLE", True)
class TestLeanstralVerifierMockedWithKernel:
    """Tests with both Leanstral and Lean kernel mocked."""

    def _make_verifier(self, mock_client: MagicMock) -> LeanstralVerifier:
        verifier = LeanstralVerifier(api_key="test-key")
        verifier._client = mock_client
        return verifier

    @patch("acgs_lite.lean_verify.subprocess.run")
    def test_kernel_verified_proof(self, mock_run: MagicMock) -> None:
        mock_run.return_value = MagicMock(returncode=0, stderr="")
        client = _mock_chat_responses(
            _make_formalization_response(),
            _make_proof_response(),
        )
        verifier = self._make_verifier(client)

        result = verifier.verify("test", _make_rules(2))

        assert result.proved is True
        assert result.certificate is not None
        assert result.certificate.kernel_verified is True
        assert result.attempts == 1

    @patch("acgs_lite.lean_verify.subprocess.run")
    def test_kernel_rejects_then_accepts(self, mock_run: MagicMock) -> None:
        """Feedback loop: first proof fails, second succeeds."""
        mock_run.side_effect = [
            # First attempt: kernel rejects
            MagicMock(returncode=1, stderr="error: type mismatch"),
            # Second attempt: kernel accepts
            MagicMock(returncode=0, stderr=""),
        ]
        client = _mock_chat_responses(
            _make_formalization_response(),
            "by simp  -- first attempt (will fail)",
            "by\n  constructor\n  · simp\n  · simp  -- fixed",
        )
        verifier = self._make_verifier(client)
        verifier._max_attempts = 3

        result = verifier.verify("test", _make_rules(2))

        assert result.proved is True
        assert result.attempts == 2
        assert result.certificate is not None
        assert result.certificate.kernel_verified is True
        assert len(result.lean_errors) > 0  # Has errors from first attempt

    @patch("acgs_lite.lean_verify.subprocess.run")
    def test_all_attempts_fail(self, mock_run: MagicMock) -> None:
        """All proof attempts rejected by kernel."""
        mock_run.return_value = MagicMock(returncode=1, stderr="error: unsolved goals")
        client = _mock_chat_responses(
            _make_formalization_response(),
            "by sorry  -- attempt 1",
            "by sorry  -- attempt 2",
            "by sorry  -- attempt 3",
        )
        verifier = self._make_verifier(client)
        verifier._max_attempts = 3

        result = verifier.verify("test", _make_rules(2))

        assert result.proved is False
        assert result.verified is True
        assert result.certificate is None
        assert result.attempts == 3
        assert "3 attempts" in (result.counterexample or "")
