"""Tests for Anti-Fake-Evidence Phase 2.

~35 tests covering:
- Audit trail extension (evidence review/create logging)
- Assertion engine (extraction, CRUD, verify, summary)
- Four-Eyes review (domain check, first/second review, same-person reject)
- UI badge data (response schema includes new fields)
"""

from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
from fastapi import FastAPI
from fastapi.testclient import TestClient

from compliance.api.evidence_routes import (
    router as evidence_router,
    _requires_four_eyes,
    _classify_confidence,
    _classify_truth_status,
)
from compliance.api.assertion_routes import router as assertion_router
from compliance.services.assertion_engine import extract_assertions, _classify_sentence
from compliance.db.models import (
    EvidenceConfidenceEnum,
    EvidenceTruthStatusEnum,
    ControlStatusEnum,
    AssertionDB,
)
from classroom_engine.database import get_db

# ---------------------------------------------------------------------------
# App setup with mocked DB dependency
# ---------------------------------------------------------------------------

app = FastAPI()
app.include_router(evidence_router)
app.include_router(assertion_router)

mock_db = MagicMock()


def override_get_db():
    yield mock_db


app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

EVIDENCE_UUID = "eeee0002-aaaa-bbbb-cccc-ffffffffffff"
CONTROL_UUID = "cccc0002-aaaa-bbbb-cccc-dddddddddddd"
ASSERTION_UUID = "aaaa0002-bbbb-cccc-dddd-eeeeeeeeeeee"
NOW = datetime(2026, 3, 23, 14, 0, 0)


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

def make_evidence(overrides=None):
    e = MagicMock()
    e.id = EVIDENCE_UUID
    e.control_id = CONTROL_UUID
    e.evidence_type = "test_results"
    e.title = "Phase 2 Test Evidence"
    e.description = "Testing four-eyes"
    e.artifact_url = "https://ci.example.com/artifact"
    e.artifact_path = None
    e.artifact_hash = "abc123"
    e.file_size_bytes = None
    e.mime_type = None
    e.status = MagicMock()
    e.status.value = "valid"
    e.uploaded_by = None
    e.source = "api"
    e.ci_job_id = None
    e.valid_from = NOW
    e.valid_until = NOW + timedelta(days=90)
    e.collected_at = NOW
    e.created_at = NOW
    e.confidence_level = EvidenceConfidenceEnum.E1
    e.truth_status = EvidenceTruthStatusEnum.UPLOADED
    e.generation_mode = None
    e.may_be_used_as_evidence = True
    e.reviewed_by = None
    e.reviewed_at = None
    # Phase 2 fields
    e.approval_status = "none"
    e.first_reviewer = None
    e.first_reviewed_at = None
    e.second_reviewer = None
    e.second_reviewed_at = None
    e.requires_four_eyes = False
    if overrides:
        for k, v in overrides.items():
            setattr(e, k, v)
    return e


def make_assertion(overrides=None):
    a = MagicMock()
    a.id = ASSERTION_UUID
    a.tenant_id = "tenant-001"
    a.entity_type = "control"
    a.entity_id = CONTROL_UUID
    a.sentence_text = "Test assertion sentence"
    a.sentence_index = 0
    a.assertion_type = "assertion"
    a.evidence_ids = []
    a.confidence = 0.0
    a.normative_tier = "pflicht"
    a.verified_by = None
    a.verified_at = None
    a.created_at = NOW
    a.updated_at = NOW
    if overrides:
        for k, v in overrides.items():
            setattr(a, k, v)
    return a


# ===========================================================================
# 1. TestAuditTrailExtension
# ===========================================================================

class TestAuditTrailExtension:
    """Test that evidence review and create log audit trail entries."""

    def test_review_evidence_logs_audit_trail(self):
        evidence = make_evidence()
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence
        mock_db.refresh.return_value = None

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"confidence_level": "E2", "reviewed_by": "auditor@test.com"},
        )
        assert resp.status_code == 200
        # db.add should be called for audit trail entries
        assert mock_db.add.called

    def test_review_evidence_records_old_and_new_confidence(self):
        evidence = make_evidence({"confidence_level": EvidenceConfidenceEnum.E1})
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence
        mock_db.refresh.return_value = None

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"confidence_level": "E3", "reviewed_by": "reviewer@test.com"},
        )
        assert resp.status_code == 200

    def test_review_evidence_records_truth_status_change(self):
        evidence = make_evidence({"truth_status": EvidenceTruthStatusEnum.UPLOADED})
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence
        mock_db.refresh.return_value = None

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"truth_status": "validated_internal", "reviewed_by": "reviewer@test.com"},
        )
        assert resp.status_code == 200

    def test_review_nonexistent_evidence_returns_404(self):
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        resp = client.patch(
            "/evidence/nonexistent/review",
            json={"reviewed_by": "someone"},
        )
        assert resp.status_code == 404

    def test_reject_evidence_logs_audit_trail(self):
        evidence = make_evidence()
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence
        mock_db.refresh.return_value = None

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/reject",
            json={"reviewed_by": "auditor@test.com", "rejection_reason": "Fake evidence"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["approval_status"] == "rejected"

    def test_reject_nonexistent_evidence_returns_404(self):
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        resp = client.patch(
            "/evidence/nonexistent/reject",
            json={"reviewed_by": "someone"},
        )
        assert resp.status_code == 404

    def test_audit_trail_query_endpoint(self):
        mock_db.reset_mock()
        trail_entry = MagicMock()
        trail_entry.id = "trail-001"
        trail_entry.entity_type = "evidence"
        trail_entry.entity_id = EVIDENCE_UUID
        trail_entry.entity_name = "Test"
        trail_entry.action = "review"
        trail_entry.field_changed = "confidence_level"
        trail_entry.old_value = "E1"
        trail_entry.new_value = "E2"
        trail_entry.change_summary = None
        trail_entry.performed_by = "auditor"
        trail_entry.performed_at = NOW
        trail_entry.checksum = "abc"
        mock_db.query.return_value.filter.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [trail_entry]

        resp = client.get(f"/audit-trail?entity_type=evidence&entity_id={EVIDENCE_UUID}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] >= 1

    def test_audit_trail_checksum_present(self):
        """Audit trail entries should have a checksum for integrity."""
        from compliance.api.audit_trail_utils import create_signature
        sig = create_signature("evidence|123|review|user@test.com")
        assert len(sig) == 64  # SHA-256 hex digest


# ===========================================================================
# 2. TestAssertionEngine
# ===========================================================================

class TestAssertionEngine:
    """Test assertion extraction and classification."""

    def test_pflicht_sentence_classified_as_assertion(self):
        result = _classify_sentence("Die Organisation muss ein ISMS implementieren.")
        assert result == ("assertion", "pflicht")

    def test_empfehlung_sentence_classified(self):
        result = _classify_sentence("Die Organisation sollte regelmäßige Audits durchführen.")
        assert result == ("assertion", "empfehlung")

    def test_kann_sentence_classified(self):
        result = _classify_sentence("Optional kann ein externes Audit durchgeführt werden.")
        assert result == ("assertion", "kann")

    def test_rationale_sentence_classified(self):
        result = _classify_sentence("Dies ist erforderlich, weil Datenverlust schwere Folgen hat.")
        assert result == ("rationale", None)

    def test_fact_sentence_with_evidence_keyword(self):
        result = _classify_sentence("Das Zertifikat wurde am 15.03.2026 ausgestellt.")
        assert result == ("fact", None)

    def test_extract_assertions_splits_sentences(self):
        text = "Die Organisation muss Daten schützen. Sie sollte regelmäßig prüfen."
        results = extract_assertions(text, "control", "ctrl-001")
        assert len(results) == 2
        assert results[0]["assertion_type"] == "assertion"
        assert results[0]["normative_tier"] == "pflicht"
        assert results[1]["normative_tier"] == "empfehlung"

    def test_extract_assertions_empty_text(self):
        results = extract_assertions("", "control", "ctrl-001")
        assert results == []

    def test_extract_assertions_single_sentence(self):
        results = extract_assertions("Der Betreiber muss ein Audit durchführen.", "control", "ctrl-001")
        assert len(results) == 1
        assert results[0]["normative_tier"] == "pflicht"

    def test_mixed_text_with_rationale(self):
        text = "Die Organisation muss ein ISMS implementieren. Dies ist notwendig, weil Compliance gefordert ist."
        results = extract_assertions(text, "control", "ctrl-001")
        assert len(results) == 2
        types = [r["assertion_type"] for r in results]
        assert "assertion" in types
        assert "rationale" in types

    def test_assertion_crud_create(self):
        mock_db.reset_mock()
        mock_db.refresh.return_value = None
        # Mock the added object to return proper values
        def side_effect_add(obj):
            obj.id = ASSERTION_UUID
            obj.created_at = NOW
            obj.updated_at = NOW
            obj.sentence_index = 0
            obj.confidence = 0.0
        mock_db.add.side_effect = side_effect_add

        resp = client.post(
            "/assertions?tenant_id=tenant-001",
            json={
                "entity_type": "control",
                "entity_id": CONTROL_UUID,
                "sentence_text": "Die Organisation muss ein ISMS implementieren.",
                "assertion_type": "assertion",
                "normative_tier": "pflicht",
            },
        )
        assert resp.status_code == 200

    def test_assertion_verify_endpoint(self):
        a = make_assertion()
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = a
        mock_db.refresh.return_value = None

        resp = client.post(f"/assertions/{ASSERTION_UUID}/verify?verified_by=auditor@test.com")
        assert resp.status_code == 200
        assert a.assertion_type == "fact"
        assert a.verified_by == "auditor@test.com"

    def test_assertion_summary(self):
        mock_db.reset_mock()
        a1 = make_assertion({"assertion_type": "assertion", "verified_by": None})
        a2 = make_assertion({"assertion_type": "fact", "verified_by": "user"})
        a3 = make_assertion({"assertion_type": "rationale", "verified_by": None})
        mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.all.return_value = [a1, a2, a3]
        # Direct .all() for no-filter case
        mock_db.query.return_value.all.return_value = [a1, a2, a3]

        resp = client.get("/assertions/summary")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total_assertions"] == 3
        assert data["total_facts"] == 1
        assert data["total_rationale"] == 1
        assert data["unverified_count"] == 1


# ===========================================================================
# 3. TestFourEyesReview
# ===========================================================================

class TestFourEyesReview:
    """Test Four-Eyes review process."""

    def test_gov_domain_requires_four_eyes(self):
        assert _requires_four_eyes("gov") is True

    def test_priv_domain_requires_four_eyes(self):
        assert _requires_four_eyes("priv") is True

    def test_ops_domain_does_not_require_four_eyes(self):
        assert _requires_four_eyes("ops") is False

    def test_sdlc_domain_does_not_require_four_eyes(self):
        assert _requires_four_eyes("sdlc") is False

    def test_first_review_sets_first_approved(self):
        evidence = make_evidence({
            "requires_four_eyes": True,
            "approval_status": "pending_first",
        })
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence
        mock_db.refresh.return_value = None

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"reviewed_by": "reviewer1@test.com"},
        )
        assert resp.status_code == 200
        assert evidence.first_reviewer == "reviewer1@test.com"
        assert evidence.approval_status == "first_approved"

    def test_second_review_different_person_approves(self):
        evidence = make_evidence({
            "requires_four_eyes": True,
            "approval_status": "first_approved",
            "first_reviewer": "reviewer1@test.com",
        })
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence
        mock_db.refresh.return_value = None

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"reviewed_by": "reviewer2@test.com"},
        )
        assert resp.status_code == 200
        assert evidence.second_reviewer == "reviewer2@test.com"
        assert evidence.approval_status == "approved"

    def test_same_person_second_review_rejected(self):
        evidence = make_evidence({
            "requires_four_eyes": True,
            "approval_status": "first_approved",
            "first_reviewer": "reviewer1@test.com",
        })
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"reviewed_by": "reviewer1@test.com"},
        )
        assert resp.status_code == 400
        assert "different" in resp.json()["detail"].lower()

    def test_already_approved_blocked(self):
        evidence = make_evidence({
            "requires_four_eyes": True,
            "approval_status": "approved",
        })
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"reviewed_by": "reviewer3@test.com"},
        )
        assert resp.status_code == 400
        assert "already" in resp.json()["detail"].lower()

    def test_rejected_evidence_cannot_be_reviewed(self):
        evidence = make_evidence({
            "requires_four_eyes": True,
            "approval_status": "rejected",
        })
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"reviewed_by": "reviewer@test.com"},
        )
        assert resp.status_code == 400

    def test_reject_endpoint(self):
        evidence = make_evidence({"requires_four_eyes": True})
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence
        mock_db.refresh.return_value = None

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/reject",
            json={"reviewed_by": "auditor@test.com", "rejection_reason": "Not authentic"},
        )
        assert resp.status_code == 200
        assert evidence.approval_status == "rejected"


# ===========================================================================
# 4. TestUIBadgeData
# ===========================================================================

class TestUIBadgeData:
    """Test that evidence response includes all Phase 2 fields."""

    def test_evidence_response_includes_approval_status(self):
        evidence = make_evidence({
            "approval_status": "first_approved",
            "first_reviewer": "reviewer1@test.com",
            "first_reviewed_at": NOW,
            "requires_four_eyes": True,
        })
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence
        mock_db.refresh.return_value = None

        resp = client.patch(
            f"/evidence/{EVIDENCE_UUID}/review",
            json={"reviewed_by": "reviewer2@test.com"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert "approval_status" in data
        assert "requires_four_eyes" in data
        assert data["requires_four_eyes"] is True

    def test_evidence_response_includes_four_eyes_fields(self):
        evidence = make_evidence({
            "requires_four_eyes": True,
            "approval_status": "approved",
            "first_reviewer": "r1@test.com",
            "first_reviewed_at": NOW,
            "second_reviewer": "r2@test.com",
            "second_reviewed_at": NOW,
        })
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = evidence

        # Use list endpoint
        mock_db.query.return_value.filter.return_value.all.return_value = [evidence]
        mock_db.query.return_value.all.return_value = [evidence]

        # Direct test via _build_evidence_response
        from compliance.api.evidence_routes import _build_evidence_response
        resp = _build_evidence_response(evidence)
        assert resp.approval_status == "approved"
        assert resp.first_reviewer == "r1@test.com"
        assert resp.second_reviewer == "r2@test.com"
        assert resp.requires_four_eyes is True

    def test_assertion_response_schema(self):
        a = make_assertion()
        mock_db.reset_mock()
        mock_db.query.return_value.filter.return_value.first.return_value = a

        resp = client.get(f"/assertions/{ASSERTION_UUID}")
        assert resp.status_code == 200
        data = resp.json()
        assert "assertion_type" in data
        assert "normative_tier" in data
        assert "evidence_ids" in data
        assert "verified_by" in data

    def test_evidence_response_includes_confidence_and_truth(self):
        evidence = make_evidence({
            "confidence_level": EvidenceConfidenceEnum.E3,
            "truth_status": EvidenceTruthStatusEnum.OBSERVED,
        })
        from compliance.api.evidence_routes import _build_evidence_response
        resp = _build_evidence_response(evidence)
        assert resp.confidence_level == "E3"
        assert resp.truth_status == "observed"

    def test_evidence_response_none_four_eyes_fields_default(self):
        evidence = make_evidence()
        from compliance.api.evidence_routes import _build_evidence_response
        resp = _build_evidence_response(evidence)
        assert resp.approval_status == "none"
        assert resp.requires_four_eyes is False
        assert resp.first_reviewer is None
