"""tests/test_crypto_attestation.py — Cryptographic attestation unit tests (Phase 2.1)."""
import json
import re
import tempfile
from pathlib import Path

import pytest

from sichgate.crypto_attestation import CryptoAttestation


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

@pytest.fixture
def att() -> CryptoAttestation:
    return CryptoAttestation()


@pytest.fixture
def sample_findings() -> dict:
    return {
        "run_id": "test-run-001",
        "model_target": "test-model",
        "results": [
            {"attack_type": "prompt_injection", "status": "fail", "severity": "high"}
        ],
    }


# ---------------------------------------------------------------------------
# 1. Key generation
# ---------------------------------------------------------------------------

def test_attestation_generates_key_on_init(att):
    assert att._private_key is not None
    assert att._public_key is not None


def test_attestation_different_keys_per_instance():
    a1 = CryptoAttestation()
    a2 = CryptoAttestation()
    assert a1.public_key_b64() != a2.public_key_b64()


def test_public_key_pem_format(att):
    pem = att.public_key_pem()
    assert pem.startswith("-----BEGIN PUBLIC KEY-----")
    assert pem.strip().endswith("-----END PUBLIC KEY-----")


def test_public_key_b64_is_base64(att):
    b64 = att.public_key_b64()
    import base64
    decoded = base64.urlsafe_b64decode(b64 + "==")
    assert len(decoded) == 32  # ed25519 public key is 32 bytes


# ---------------------------------------------------------------------------
# 2. Key persistence
# ---------------------------------------------------------------------------

def test_save_and_load_key_produces_same_public_key(tmp_path):
    key_file = str(tmp_path / "key.pem")
    a1 = CryptoAttestation()
    a1.save_key(key_file)
    a2 = CryptoAttestation(key_path=key_file)
    assert a1.public_key_b64() == a2.public_key_b64()


def test_save_key_creates_file(tmp_path, att):
    key_file = str(tmp_path / "key.pem")
    att.save_key(key_file)
    assert Path(key_file).exists()


def test_load_nonexistent_key_generates_fresh(tmp_path):
    nonexistent = str(tmp_path / "nokey.pem")
    a = CryptoAttestation(key_path=nonexistent)
    assert a._private_key is not None  # generated fresh


# ---------------------------------------------------------------------------
# 3. sign_findings_report
# ---------------------------------------------------------------------------

def test_sign_report_returns_dict(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    assert isinstance(signed, dict)


def test_sign_report_has_attestation_block(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    assert "attestation" in signed


def test_sign_report_preserves_original_findings(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    assert signed["run_id"] == sample_findings["run_id"]
    assert signed["model_target"] == sample_findings["model_target"]


def test_sign_report_attestation_has_signature(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    assert "signature" in signed["attestation"]
    assert len(signed["attestation"]["signature"]) > 0


def test_sign_report_attestation_has_timestamp(att, sample_findings):
    from datetime import datetime
    signed = att.sign_findings_report(sample_findings)
    ts = signed["attestation"]["timestamp"]
    datetime.fromisoformat(ts.replace("Z", "+00:00"))


def test_sign_report_attestation_has_public_key(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    assert "public_key_b64" in signed["attestation"]
    assert "public_key_pem" in signed["attestation"]


def test_sign_report_attestation_id_is_uuid(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    attest_id = signed["attestation"]["attestation_id"]
    pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
    assert re.match(pattern, attest_id)


def test_sign_report_payload_hash_is_sha256(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    payload_hash = signed["attestation"]["payload_hash"]
    assert len(payload_hash) == 64
    assert all(c in "0123456789abcdef" for c in payload_hash)


def test_sign_report_algorithm_is_ed25519(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    assert signed["attestation"]["algorithm"] == "ed25519"


# ---------------------------------------------------------------------------
# 4. Verification
# ---------------------------------------------------------------------------

def test_verify_report_valid_signature(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    assert att.verify_report(signed) is True


def test_verify_report_tampered_findings_fails(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    # Tamper with a field
    signed["run_id"] = "TAMPERED"
    assert att.verify_report(signed) is False


def test_verify_report_tampered_hash_fails(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    signed["attestation"]["payload_hash"] = "a" * 64
    assert att.verify_report(signed) is False


def test_verify_external_with_embedded_key(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    assert CryptoAttestation.verify_external(signed) is True


def test_verify_external_tampered_fails(att, sample_findings):
    signed = att.sign_findings_report(sample_findings)
    signed["results"] = [{"injected": "data"}]
    assert CryptoAttestation.verify_external(signed) is False


def test_verify_external_missing_attestation_returns_false():
    assert CryptoAttestation.verify_external({"run_id": "x"}) is False


# ---------------------------------------------------------------------------
# 5. sign_status_transition
# ---------------------------------------------------------------------------

def test_sign_status_transition_has_required_fields(att):
    entry = att.sign_status_transition("find-001", "NEW", "MITIGATED")
    assert entry["finding_id"] == "find-001"
    assert entry["from_status"] == "NEW"
    assert entry["to_status"] == "MITIGATED"
    assert "signature" in entry
    assert "timestamp" in entry


def test_sign_status_transition_uses_custom_actor(att):
    entry = att.sign_status_transition("find-001", "NEW", "MITIGATED", actor="compliance_officer")
    assert entry["actor"] == "compliance_officer"


def test_sign_status_transition_default_actor_is_sichgate(att):
    entry = att.sign_status_transition("find-001", "NEW", "RESOLVED")
    assert entry["actor"] == "sichgate"


# ---------------------------------------------------------------------------
# 6. Audit trail
# ---------------------------------------------------------------------------

def test_build_audit_trail_initial_status(att):
    trail = att.build_audit_trail("find-001", initial_status="NEW")
    assert trail["current_status"] == "NEW"
    assert trail["finding_id"] == "find-001"
    assert len(trail["status_history"]) == 1


def test_append_transition_updates_status(att):
    trail = att.build_audit_trail("find-002", initial_status="NEW")
    trail = att.append_transition(trail, "MITIGATED")
    assert trail["current_status"] == "MITIGATED"
    assert len(trail["status_history"]) == 2


def test_append_transition_does_not_mutate_original(att):
    trail = att.build_audit_trail("find-003", initial_status="NEW")
    original_len = len(trail["status_history"])
    _ = att.append_transition(trail, "RESOLVED")
    assert len(trail["status_history"]) == original_len


def test_audit_trail_chain_all_signed(att):
    trail = att.build_audit_trail("find-004", initial_status="NEW")
    trail = att.append_transition(trail, "MITIGATED")
    trail = att.append_transition(trail, "RESOLVED")
    for entry in trail["status_history"]:
        assert "signature" in entry
        assert "public_key_b64" in entry
