"""
tests/test_baseline_profiler.py — Gap #1: Comparative Vulnerability Profiling.

Tests BaselineProfiler, DeltaReport, Delta, and Attribution classes.
Uses callable adapter for speed (no model loading required).
"""
import json
import uuid
from pathlib import Path
from unittest.mock import MagicMock, patch
from tempfile import TemporaryDirectory

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from sichgate.database import Base, DatabaseManager
from sichgate.profilers.baseline_profiler import (
    BaselineProfiler,
    BaselineProfile,
    Delta,
    DeltaReport,
    Attribution,
    _FIXED_THRESHOLD,
    _REGRESSED_THRESHOLD,
)
from sichgate.schema import now_iso


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

@pytest.fixture
def in_memory_db():
    """Return a temp-dir-backed DatabaseManager (in-memory SQLite)."""
    with TemporaryDirectory() as tmpdir:
        db_path = Path(tmpdir) / "test.db"
        manager = DatabaseManager(db_path)
        yield manager, db_path


@pytest.fixture
def profiler(in_memory_db):
    """BaselineProfiler backed by temp database."""
    _, db_path = in_memory_db
    return BaselineProfiler(db_path=db_path)


def _mock_attack_result(attack_name: str, success_rate: float, status: str = "fail") -> dict:
    return {
        "attack_type": "llm",
        "attack_name": attack_name,
        "model_target": "test_model",
        "timestamp": now_iso(),
        "status": status,
        "severity": "high" if status == "fail" else None,
        "input_payload": "test prompt",
        "model_response": "response",
        "notes": "test note",
        "compliance_tags": [],
        "execution_telemetry": {
            "status": "completed", "duration_ms": 50,
            "attacks_attempted": 1, "attacks_completed": 1,
            "model_reachable": True, "reason": None,
        },
        "success_rate": success_rate,
        "attack_category": "jailbreak",
        "turn_number": None,
        "safety_score": None,
        "prompt_embedding": None,
        "remediation": None,
    }


# ---------------------------------------------------------------------------
# Unit tests: compute_delta
# ---------------------------------------------------------------------------

class TestComputeDelta:
    def test_fixed_classification(self, profiler):
        """Success rate drops by >15% → FIXED."""
        base = _mock_attack_result("jailbreak", 0.60)
        ft = _mock_attack_result("jailbreak", 0.30)
        delta = profiler.compute_delta(base, ft)

        assert delta.classification == "FIXED"
        assert delta.delta_success_rate < 0
        assert delta.confidence > 0.0

    def test_regressed_classification(self, profiler):
        """Success rate rises by >10% → REGRESSED."""
        base = _mock_attack_result("jailbreak", 0.10)
        ft = _mock_attack_result("jailbreak", 0.50)
        delta = profiler.compute_delta(base, ft)

        assert delta.classification == "REGRESSED"
        assert delta.delta_success_rate > 0
        assert delta.confidence > 0.0

    def test_neutral_classification(self, profiler):
        """Small change → NEUTRAL."""
        base = _mock_attack_result("jailbreak", 0.50)
        ft = _mock_attack_result("jailbreak", 0.53)
        delta = profiler.compute_delta(base, ft)

        assert delta.classification == "NEUTRAL"
        assert abs(delta.delta_success_rate) < _REGRESSED_THRESHOLD

    def test_delta_preserves_attack_metadata(self, profiler):
        """Delta carries attack_name and attack_type from base result."""
        base = _mock_attack_result("instruction_hijacking", 0.70)
        ft = _mock_attack_result("instruction_hijacking", 0.20)
        delta = profiler.compute_delta(base, ft)

        assert delta.attack_name == "instruction_hijacking"
        assert delta.attack_type == "llm"

    def test_delta_to_dict(self, profiler):
        """Delta.to_dict() returns serializable dict."""
        base = _mock_attack_result("jailbreak", 0.5)
        ft = _mock_attack_result("jailbreak", 0.1)
        delta = profiler.compute_delta(base, ft)
        d = delta.to_dict()

        assert "classification" in d
        assert "base_result" in d
        assert "ft_result" in d
        assert "attribution" in d
        assert isinstance(d, dict)


# ---------------------------------------------------------------------------
# Unit tests: attribution_analysis
# ---------------------------------------------------------------------------

class TestAttributionAnalysis:
    def test_safeguard_attribution_high_base_rate(self, profiler):
        """Base rate >0.5 that improves → fine-tuning safeguard."""
        base = _mock_attack_result("jailbreak", 0.70)
        attr = profiler.attribution_analysis(-0.40, base)
        assert attr.source == "fine-tuning-safeguard"
        assert attr.confidence > 0.5

    def test_introduced_regression_low_base_rate(self, profiler):
        """Base rate <0.3 that worsens → fine-tuning introduced."""
        base = _mock_attack_result("jailbreak", 0.15)
        attr = profiler.attribution_analysis(+0.30, base)
        assert attr.source == "fine-tuning-introduced"
        assert attr.confidence > 0.5

    def test_base_artifact_attribution(self, profiler):
        """Already high base rate that worsens further → base-model-artifact."""
        base = _mock_attack_result("jailbreak", 0.60)
        attr = profiler.attribution_analysis(+0.10, base)
        assert attr.source == "base-model-artifact"

    def test_attribution_has_explanation(self, profiler):
        """Attribution always provides an explanation string."""
        base = _mock_attack_result("jailbreak", 0.50)
        attr = profiler.attribution_analysis(-0.20, base)
        assert len(attr.explanation) > 10


# ---------------------------------------------------------------------------
# Unit tests: DeltaReport
# ---------------------------------------------------------------------------

class TestDeltaReport:
    def _make_delta(self, classification: str, base_rate: float, ft_rate: float) -> Delta:
        return Delta(
            attack_type="llm",
            attack_name="test_attack",
            base_result={"success_rate": base_rate, "status": "fail", "severity": "high"},
            ft_result={"success_rate": ft_rate, "status": "pass", "severity": None},
            classification=classification,
            confidence=0.85,
            attribution=Attribution(
                source="fine-tuning-safeguard", confidence=0.85,
                explanation="Test explanation"
            ),
            delta_success_rate=ft_rate - base_rate,
        )

    def test_to_dict_structure(self):
        """DeltaReport.to_dict() has expected top-level keys."""
        report = DeltaReport(
            report_id=str(uuid.uuid4()),
            baseline_id="baseline-abc",
            baseline_model="tinyllama",
            ft_model="my-ft-model",
            ft_model_hash="abc123",
            ft_data_path=None,
            created_at=now_iso(),
            deltas=[self._make_delta("FIXED", 0.7, 0.2)],
            summary={"fixed": 1, "regressed": 0, "neutral": 0},
        )
        d = report.to_dict()

        assert "report_id" in d
        assert "baseline" in d
        assert "fine_tuned" in d
        assert "deltas" in d
        assert "summary" in d
        assert len(d["deltas"]) == 1

    def test_summary_counts_correct(self):
        """Summary correctly counts FIXED/REGRESSED/NEUTRAL."""
        deltas = [
            self._make_delta("FIXED", 0.7, 0.2),
            self._make_delta("FIXED", 0.6, 0.1),
            self._make_delta("REGRESSED", 0.1, 0.5),
            self._make_delta("NEUTRAL", 0.5, 0.5),
        ]
        report = DeltaReport(
            report_id=str(uuid.uuid4()),
            baseline_id="x",
            baseline_model="base",
            ft_model="ft",
            ft_model_hash=None,
            ft_data_path=None,
            created_at=now_iso(),
            deltas=deltas,
            summary={"fixed": 2, "regressed": 1, "neutral": 1},
        )
        assert report.summary["fixed"] == 2
        assert report.summary["regressed"] == 1
        assert report.summary["neutral"] == 1


# ---------------------------------------------------------------------------
# Integration tests: BaselineProfiler with mocked AttackRunner
# ---------------------------------------------------------------------------

class TestBaselineProfilerIntegration:
    """
    Integration tests using sys.modules injection to mock sichgate.runner
    without triggering its problematic top-level imports.
    """

    def _make_mock_summary(self, attack_names: list, success_rates: list):
        """Build a mock RunSummary-like object."""
        results = [
            _mock_attack_result(name, rate)
            for name, rate in zip(attack_names, success_rates)
        ]
        mock_summary = MagicMock()
        mock_summary.results = results
        return mock_summary

    def _runner_modules_patch(self, summary):
        """Return a context manager that injects a mock sichgate.runner module."""
        import sys
        mock_runner_instance = MagicMock()
        mock_runner_instance.run.return_value = summary

        mock_runner_cls = MagicMock(return_value=mock_runner_instance)
        mock_runner_module = MagicMock()
        mock_runner_module.AttackRunner = mock_runner_cls

        return patch.dict("sys.modules", {"sichgate.runner": mock_runner_module})

    def test_profile_base_model_stores_baseline(self, profiler, in_memory_db):
        """profile_base_model() persists a BaselineProfile in the database."""
        _, db_path = in_memory_db
        summary = self._make_mock_summary(["jailbreak_roleplay"], [0.50])

        adapter = MagicMock()
        adapter.model_path = "test-base-model"
        adapter.adapter_type = "callable"

        with self._runner_modules_patch(summary):
            baseline = profiler.profile_base_model(
                adapter=adapter,
                attack_classes=[MagicMock(__name__="MockAttack")],
            )

        assert isinstance(baseline, BaselineProfile)
        assert baseline.model_id == "test-base-model"
        assert isinstance(baseline.id, str)
        assert len(baseline.results) == 1

        # Verify persistence
        loaded = profiler.get_baseline(baseline.id)
        assert loaded is not None
        assert loaded.model_id == "test-base-model"

    def test_profile_fine_tuned_model_computes_deltas(self, profiler):
        """profile_fine_tuned_model() returns a DeltaReport with correct classifications."""
        baseline = BaselineProfile(
            id=str(uuid.uuid4()),
            model_id="base-model",
            model_hash="abc",
            model_version="1.0",
            created_at=now_iso(),
            attacks_run=["jailbreak_roleplay", "prompt_injection_direct"],
            results=[
                _mock_attack_result("jailbreak_roleplay", 0.70),
                _mock_attack_result("prompt_injection_direct", 0.20),
            ],
        )
        profiler._persist_baseline(baseline)

        ft_summary = self._make_mock_summary(
            ["jailbreak_roleplay", "prompt_injection_direct"],
            [0.20, 0.60],  # jailbreak improved → FIXED; injection worsened → REGRESSED
        )

        adapter = MagicMock()
        adapter.model_path = "ft-model"
        adapter.adapter_type = "callable"

        with self._runner_modules_patch(ft_summary):
            delta_report = profiler.profile_fine_tuned_model(
                adapter=adapter,
                baseline_id=baseline.id,
                attack_classes=[MagicMock(), MagicMock()],
            )

        assert isinstance(delta_report, DeltaReport)
        assert delta_report.summary["fixed"] >= 1
        assert delta_report.summary["regressed"] >= 1

    def test_missing_baseline_raises(self, profiler):
        """profile_fine_tuned_model() raises ValueError for unknown baseline_id."""
        adapter = MagicMock()
        adapter.model_path = "ft-model"

        with pytest.raises(ValueError, match="not found"):
            profiler.profile_fine_tuned_model(
                adapter=adapter,
                baseline_id="nonexistent-id",
                attack_classes=[],
            )

    def test_list_baselines_empty(self, profiler):
        """list_baselines() returns empty list when no baselines stored."""
        assert profiler.list_baselines() == []

    def test_list_baselines_returns_stored(self, profiler):
        """list_baselines() returns summaries of stored baselines."""
        summary = self._make_mock_summary(["jailbreak"], [0.5])
        adapter = MagicMock()
        adapter.model_path = "tinyllama"
        adapter.adapter_type = "callable"

        with self._runner_modules_patch(summary):
            profiler.profile_base_model(adapter=adapter, attack_classes=[MagicMock()])

        baselines = profiler.list_baselines()
        assert len(baselines) == 1
        assert baselines[0]["model_id"] == "tinyllama"


# ---------------------------------------------------------------------------
# Unit tests: BaselineProfile
# ---------------------------------------------------------------------------

class TestBaselineProfile:
    def test_get_attack_success_rate(self):
        profile = BaselineProfile(
            id="x", model_id="m", model_hash=None, model_version=None,
            created_at=now_iso(),
            attacks_run=["jailbreak"],
            results=[_mock_attack_result("jailbreak", 0.45)],
        )
        assert profile.get_attack_success_rate("jailbreak") == pytest.approx(0.45)
        assert profile.get_attack_success_rate("unknown_attack") is None

    def test_from_dict_round_trip(self):
        profile = BaselineProfile(
            id="abc", model_id="tinyllama", model_hash="x123",
            model_version="1.0", created_at=now_iso(),
            attacks_run=["jailbreak"], results=[],
        )
        d = profile.to_dict()
        restored = BaselineProfile.from_dict(d)
        assert restored.id == "abc"
        assert restored.model_id == "tinyllama"
