"""tests/test_differential_redteaming.py — Differential redteaming unit tests (Phase 2.1)."""
import json
from pathlib import Path

import pytest

from sichgate.differential_redteaming import (
    AttackDelta,
    DifferentialRedteaming,
    DifferentialReport,
    _DELTA_THRESHOLD,
)


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

def _make_result(attack_type: str, success_rate: float, severity: str = "medium") -> dict:
    return {
        "attack_type": attack_type,
        "attack_name": f"test_{attack_type}",
        "status": "fail" if success_rate > 0.5 else "pass",
        "severity": severity if success_rate > 0.5 else None,
        "success_rate": success_rate,
        "compliance_tags": [],
    }


@pytest.fixture
def base_results():
    return [
        _make_result("prompt_injection", 0.35),
        _make_result("bias_fairness", 0.10),
        _make_result("hallucination", 0.20),
    ]


@pytest.fixture
def custom_results_worsened():
    """Fine-tuned model is significantly worse on prompt injection."""
    return [
        _make_result("prompt_injection", 0.72, severity="high"),  # +0.37 → CRITICAL
        _make_result("bias_fairness", 0.12),                       # +0.02 → UNCHANGED
        _make_result("hallucination", 0.08),                       # −0.12 → IMPROVED
    ]


@pytest.fixture
def engine() -> DifferentialRedteaming:
    return DifferentialRedteaming()


@pytest.fixture
def report(engine, base_results, custom_results_worsened):
    return engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="qwen2-1.5b",
        custom_model_id="medical-finetuned",
    )


# ---------------------------------------------------------------------------
# 1. AttackDelta computation
# ---------------------------------------------------------------------------

def test_delta_computed_correctly(engine, base_results, custom_results_worsened):
    report = engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="base",
        custom_model_id="custom",
    )
    pi = report.differential_analysis["prompt_injection"]
    assert abs(pi.delta - (0.72 - 0.35)) < 0.01


def test_delta_trend_worsened(engine, base_results, custom_results_worsened):
    report = engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="base",
        custom_model_id="custom",
    )
    assert report.differential_analysis["prompt_injection"].trend == "WORSENED"


def test_delta_trend_improved(engine, base_results, custom_results_worsened):
    report = engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="base",
        custom_model_id="custom",
    )
    assert report.differential_analysis["hallucination"].trend == "IMPROVED"


def test_delta_trend_unchanged_small_delta(engine):
    base = [_make_result("alignment", 0.30)]
    custom = [_make_result("alignment", 0.32)]  # +0.02 < threshold
    report = engine.compare(
        base_results=base, custom_results=custom,
        base_model_id="b", custom_model_id="c",
    )
    assert report.differential_analysis["alignment"].trend == "UNCHANGED"


def test_delta_trend_boundary_at_threshold(engine):
    base = [_make_result("alignment", 0.30)]
    custom = [_make_result("alignment", 0.30 + _DELTA_THRESHOLD)]
    report = engine.compare(
        base_results=base, custom_results=custom,
        base_model_id="b", custom_model_id="c",
    )
    assert report.differential_analysis["alignment"].trend == "WORSENED"


def test_delta_result_counts_correct(report):
    pi = report.differential_analysis["prompt_injection"]
    assert pi.base_result_count == 1
    assert pi.custom_result_count == 1


# ---------------------------------------------------------------------------
# 2. Severity tracking
# ---------------------------------------------------------------------------

def test_worst_severity_base(engine):
    base = [
        _make_result("prompt_injection", 0.80),  # severity="medium"
    ]
    custom = [_make_result("prompt_injection", 0.20)]
    base[0]["severity"] = "high"
    report = engine.compare(base_results=base, custom_results=custom, base_model_id="b", custom_model_id="c")
    assert report.differential_analysis["prompt_injection"].base_severity == "high"


def test_worst_severity_none_when_no_results(engine):
    report = engine.compare(
        base_results=[],
        custom_results=[_make_result("prompt_injection", 0.5)],
        base_model_id="b",
        custom_model_id="c",
    )
    assert report.differential_analysis["prompt_injection"].base_severity is None


# ---------------------------------------------------------------------------
# 3. Compliance alerts
# ---------------------------------------------------------------------------

def test_critical_alert_generated_for_large_delta(report):
    pi = report.differential_analysis["prompt_injection"]
    assert pi.compliance_alert is not None
    assert "CRITICAL" in pi.compliance_alert


def test_no_alert_for_small_delta(report):
    bf = report.differential_analysis["bias_fairness"]
    assert bf.compliance_alert is None


def test_critical_alerts_list_populated(report):
    assert len(report.critical_alerts) >= 1
    assert "prompt_injection" in report.critical_alerts[0]


def test_annex_iv_sections_in_delta(report):
    pi = report.differential_analysis["prompt_injection"]
    assert len(pi.annex_iv_sections) > 0
    assert "2g" in pi.annex_iv_sections


def test_eu_ai_act_article_in_delta(report):
    pi = report.differential_analysis["prompt_injection"]
    assert pi.eu_ai_act_article.startswith("Article")


# ---------------------------------------------------------------------------
# 4. DifferentialReport structure
# ---------------------------------------------------------------------------

def test_report_has_run_id(report):
    import re
    pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-"
    assert re.match(pattern, report.run_id)


def test_report_base_model_id(report):
    assert report.base_model_id == "qwen2-1.5b"


def test_report_custom_model_id(report):
    assert report.custom_model_id == "medical-finetuned"


def test_report_timestamp_is_iso(report):
    from datetime import datetime
    datetime.fromisoformat(report.timestamp.replace("Z", "+00:00"))


def test_report_overall_risk_delta_is_float(report):
    assert isinstance(report.overall_risk_delta, float)


def test_report_annex_iv_annotations_populated(report):
    assert "prompt_injection" in report.annex_iv_annotations
    assert len(report.annex_iv_annotations["prompt_injection"]) > 0


# ---------------------------------------------------------------------------
# 5. Serialisation
# ---------------------------------------------------------------------------

def test_report_to_dict_has_differential_analysis(report):
    d = report.to_dict()
    assert "differential_analysis" in d
    assert "prompt_injection" in d["differential_analysis"]


def test_report_to_json_valid(report):
    raw = report.to_json()
    parsed = json.loads(raw)
    assert "differential_analysis" in parsed


def test_report_save_creates_file(report, tmp_path):
    dest = str(tmp_path / "diff.json")
    report.save(dest)
    assert Path(dest).exists()
    parsed = json.loads(Path(dest).read_text())
    assert "run_id" in parsed


# ---------------------------------------------------------------------------
# 6. Cryptographic attestation integration
# ---------------------------------------------------------------------------

def test_report_attestation_when_signed(engine, base_results, custom_results_worsened):
    from sichgate.crypto_attestation import CryptoAttestation
    att = CryptoAttestation()
    report = engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="base",
        custom_model_id="custom",
        attestation=att,
    )
    assert report.attestation is not None
    assert "signature" in report.attestation


def test_report_no_attestation_by_default(report):
    assert report.attestation is None


def test_attestation_verifiable_externally(engine, base_results, custom_results_worsened):
    from sichgate.crypto_attestation import CryptoAttestation
    att = CryptoAttestation()
    report = engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="base",
        custom_model_id="custom",
        attestation=att,
    )
    # Wrap attestation block back into a signed_report dict for verification
    report_dict = report.to_dict()
    assert CryptoAttestation.verify_external(report_dict) is True


# ---------------------------------------------------------------------------
# 7. AIBoM embedding
# ---------------------------------------------------------------------------

def test_report_embeds_base_aibom(engine, base_results, custom_results_worsened):
    fake_bom = {"bomFormat": "CycloneDX", "specVersion": "1.6"}
    report = engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="base",
        custom_model_id="custom",
        base_aibom=fake_bom,
    )
    assert report.base_aibom == fake_bom


def test_report_embeds_custom_aibom(engine, base_results, custom_results_worsened):
    fake_bom = {"bomFormat": "CycloneDX", "specVersion": "1.6"}
    report = engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="base",
        custom_model_id="custom",
        custom_aibom=fake_bom,
    )
    assert report.custom_aibom == fake_bom


# ---------------------------------------------------------------------------
# 8. Edge cases
# ---------------------------------------------------------------------------

def test_empty_base_results(engine, custom_results_worsened):
    report = engine.compare(
        base_results=[],
        custom_results=custom_results_worsened,
        base_model_id="b",
        custom_model_id="c",
    )
    assert len(report.differential_analysis) > 0


def test_empty_both_results(engine):
    report = engine.compare(
        base_results=[],
        custom_results=[],
        base_model_id="b",
        custom_model_id="c",
    )
    assert report.overall_risk_delta == 0.0
    assert len(report.differential_analysis) == 0


def test_attestation_failure_does_not_raise(engine, base_results, custom_results_worsened):
    """A broken attestation object must not crash report generation."""
    class BrokenAttestation:
        def sign_findings_report(self, findings):
            raise RuntimeError("signing failed")

    report = engine.compare(
        base_results=base_results,
        custom_results=custom_results_worsened,
        base_model_id="b",
        custom_model_id="c",
        attestation=BrokenAttestation(),
    )
    assert report is not None
    assert report.attestation is None
