from __future__ import annotations

import json
import re
from collections.abc import Sequence
from typing import Any

import pytest

from ops.audit_kb_rebuild_coverage import (
    CommandResult,
    audit_database,
    build_audit_sql,
    build_report,
)


def aggregate_fixture() -> dict[str, Any]:
    return {
        "identity": {"database": "teleo", "transaction_read_only": "on", "server_version_num": "160004"},
        "canonical_counts": {"claims": 10, "sources": 8, "claim_evidence": 12, "claim_edges": 9},
        "staging_counts": {
            "canonical_mappings": 17,
            "import_runs": 1,
            "inventory_items": 20,
            "staged_claims": 11,
            "staged_sources": 8,
            "staged_claim_sources": 13,
            "staged_claim_edges": 10,
            "kb_proposals": 4,
        },
        "mappings": {
            "claims": {
                "rows": 9,
                "legacy_keys": 9,
                "target_ids": 9,
                "missing_canonical_targets": 0,
                "with_first_run_staged_row": 9,
                "canonical_target_rows_covered": 9,
                "canonical_target_rows_unmapped": 1,
                "staged_distinct_keys": 10,
                "staged_distinct_keys_unmapped": 1,
            },
            "sources": {
                "rows": 8,
                "legacy_keys": 8,
                "target_ids": 8,
                "missing_canonical_targets": 0,
                "with_first_run_staged_row": 7,
                "canonical_target_rows_covered": 8,
                "canonical_target_rows_unmapped": 0,
                "staged_distinct_keys": 8,
                "staged_distinct_keys_unmapped": 0,
            },
            "unsupported_target_rows": 0,
        },
        "claim_parity": {
            "compared_fields": ["type", "text_from_title"],
            "eligible_rows": 9,
            "exact_projection_rows": 9,
            "field_matches": {"type": 9, "text_from_title": 9},
            "projection_limits": {"staged_body_nonempty": 9},
        },
        "source_parity": {
            "compared_fields": ["source_type", "hash"],
            "eligible_rows": 7,
            "exact_projection_rows": 6,
            "field_matches": {"source_type": 7, "hash": 6},
            "projection_limits": {"staged_title_nonempty": 7},
        },
        "claim_evidence_reconstruction": {
            "compared_fields": ["claim_id", "source_id", "role", "weight"],
            "not_replayed_fields": ["created_by", "created_at"],
            "staged_rows": 13,
            "rows_with_mapped_claim": 12,
            "rows_with_mapped_source": 13,
            "rows_with_both_mapped_endpoints": 12,
            "distinct_projected_rows": 11,
            "projected_rows_present_in_canonical": 11,
            "canonical_rows_covered": 11,
            "canonical_rows_total": 12,
        },
        "claim_edge_reconstruction": {
            "compared_fields": ["from_claim", "to_claim", "edge_type"],
            "not_replayed_fields": ["id", "weight", "created_by", "created_at", "duplicate_multiplicity"],
            "staged_rows": 10,
            "resolution_state_resolved_rows": 9,
            "resolved_rows_with_from_mapping": 9,
            "resolved_rows_with_to_mapping": 8,
            "resolved_rows_with_both_mapped_endpoints": 8,
            "distinct_projected_rows": 8,
            "projected_rows_present_in_canonical": 8,
            "canonical_rows_covered": 8,
            "canonical_rows_total": 9,
        },
        "later_and_unmapped": {
            "latest_import_completed_at": "2026-01-01T00:00:00+00:00",
            "canonical_created_after_latest_import": {
                "claims": 1,
                "sources": 0,
                "claim_evidence": 1,
                "claim_edges": 1,
            },
            "canonical_without_mapping": {"claims": 1, "sources": 0},
            "canonical_relations_not_reconstructible": {"claim_evidence": 1, "claim_edges": 1},
            "staged_distinct_keys_without_mapping": {"claims": 1, "sources": 0},
        },
        "proposal_apply_payload": {
            "total_rows": 4,
            "status_counts": {"applied": 2, "pending_review": 2},
            "with_apply_payload_key": 2,
            "with_valid_apply_payload": 2,
            "applied_rows": 2,
            "applied_with_valid_apply_payload": 1,
        },
        "import_runs": [
            {
                "id": "run-id",
                "label": "fixture-run",
                "repo_path": "/workspace",
                "inventory_path": "/inventory/fixture.jsonl",
                "started_at": "2026-01-01T00:00:00+00:00",
                "completed_at": "2026-01-01T00:01:00+00:00",
                "inventory_items": 20,
                "staged_claims": 11,
                "staged_sources": 8,
                "staged_claim_sources": 13,
                "staged_claim_edges": 10,
            }
        ],
    }


def test_sql_is_guarded_and_read_only() -> None:
    sql = build_audit_sql()

    assert "BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY" in sql
    assert "current_setting('transaction_read_only')" in sql
    assert "expected_schema_absent" in sql
    assert "\\if :schema_ok" in sql
    assert sql.count("ROLLBACK;") >= 2
    assert not re.search(r"\b(INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE|COPY)\b", sql, re.IGNORECASE)


def test_report_separates_snapshot_recovery_from_source_recompile_and_calculates_gaps() -> None:
    report = build_report(aggregate_fixture(), container="teleo-test", database="teleo")
    semantic = report["source_derived_recompile_readiness"]

    assert report["audit_status"] == "complete"
    assert report["exact_snapshot_recovery"]["verified_by_this_database_only_auditor"] is False
    assert report["exact_snapshot_recovery"]["current_state_counts"]["canonical"]["claims"] == 10
    assert semantic["status"] == "partial"
    assert semantic["canonical_mappings"]["claims"]["canonical_coverage"] == {
        "covered": 9,
        "total": 10,
        "gap": 1,
        "percent": 90.0,
    }
    assert semantic["canonical_mappings"]["sources"]["first_run_stage_origin_coverage"]["percent"] == 87.5
    assert semantic["claim_evidence_from_staged_claim_sources"]["canonical_reconstruction_coverage"]["percent"] == 91.67
    assert semantic["claim_evidence_from_staged_claim_sources"]["not_replayed_fields"] == [
        "created_by",
        "created_at",
    ]
    assert "duplicate_multiplicity" in semantic["claim_edges_from_resolved_staged_claim_edges"]["not_replayed_fields"]
    assert semantic["proposal_apply_payload_coverage"]["applied_proposal_payload_coverage"]["percent"] == 50.0
    gap_codes = {gap["code"] for gap in semantic["explicit_gaps"]}
    assert "canonical_claims_without_mapping" in gap_codes
    assert "source_mappings_without_first_run_stage" in gap_codes
    assert "mapped_source_projection_mismatch" in gap_codes
    assert "canonical_claim_evidence_not_reconstructible" in gap_codes
    assert "canonical_claim_edges_not_reconstructible" in gap_codes
    assert "applied_proposals_without_valid_apply_payload" in gap_codes
    assert semantic["import_run_metadata"][0]["inventory_path"] == "/inventory/fixture.jsonl"
    assert report["safety"]["contains_private_bodies_or_source_excerpts"] is False


def test_audit_uses_named_container_database_and_injected_runner() -> None:
    captured: dict[str, Any] = {}

    def fake_runner(command: Sequence[str], stdin: str, timeout: int) -> CommandResult:
        captured.update(command=list(command), stdin=stdin, timeout=timeout)
        return CommandResult(0, json.dumps(aggregate_fixture()) + "\n", "")

    report = audit_database(
        container="teleo-local",
        database="teleo",
        db_user="postgres",
        timeout=45,
        runner=fake_runner,
    )

    assert captured["command"] == [
        "docker",
        "exec",
        "-i",
        "teleo-local",
        "psql",
        "-X",
        "-U",
        "postgres",
        "-d",
        "teleo",
        "-Atq",
        "-v",
        "ON_ERROR_STOP=1",
    ]
    assert captured["timeout"] == 45
    assert "READ ONLY" in captured["stdin"]
    assert report["target"]["container"] == "teleo-local"
    assert report["safety"]["transaction_read_only_readback"] == "on"


def test_missing_schema_fails_closed_with_structured_json() -> None:
    def fake_runner(_command: Sequence[str], _stdin: str, _timeout: int) -> CommandResult:
        payload = {
            "status": "error",
            "error": "expected_schema_absent",
            "missing_relations": ["kb_stage.canonical_mappings"],
            "missing_columns": ["kb_stage.canonical_mappings.legacy_key"],
        }
        return CommandResult(3, json.dumps(payload) + "\n", "")

    report = audit_database(container="empty-postgres", database="teleo", runner=fake_runner)

    assert report["audit_status"] == "error"
    assert report["error"] == "expected_schema_absent"
    assert report["details"]["missing_relations"] == ["kb_stage.canonical_mappings"]
    assert report["details"]["missing_columns"] == ["kb_stage.canonical_mappings.legacy_key"]
    assert "source_derived_recompile_readiness" not in report


def test_invalid_docker_identifier_is_rejected_before_runner() -> None:
    called = False

    def fake_runner(_command: Sequence[str], _stdin: str, _timeout: int) -> CommandResult:
        nonlocal called
        called = True
        return CommandResult(0, "{}", "")

    with pytest.raises(ValueError, match="container must match"):
        audit_database(container="--privileged", database="teleo", runner=fake_runner)

    assert called is False
