"""
tests/test_cli_technical_file.py — Integration tests for `sichgate technical-file` CLI command.

Tests:
  - Full CLI workflow end-to-end
  - With/without differential report
  - With/without attestation
  - Output files exist and are valid
  - Missing findings file raises exit code 1
  - Findings as a RunSummary dict (with "results" key)
  - Findings as a raw list
  - AIBoM embedded in findings file
  - AIBoM from separate file
"""
from __future__ import annotations

import json
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest
from typer.testing import CliRunner

from sichgate.cli import app


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

runner = CliRunner()


def _make_finding(attack_type: str = "prompt_injection", status: str = "fail") -> dict:
    return {
        "attack_name": f"test_{attack_type}",
        "attack_type": attack_type,
        "status": status,
        "severity": "high" if status == "fail" else None,
        "notes": f"CLI integration test finding for {attack_type}",
        "remediation": "Apply guardrails" if status == "fail" else None,
        "compliance_tags": ["EU-AI-ACT-ART-15"],
        "success_rate": 0.7 if status == "fail" else 0.1,
    }


def _sample_aibom() -> dict:
    return {
        "bomFormat": "CycloneDX",
        "specVersion": "1.6",
        "serialNumber": "urn:uuid:cli-test-001",
        "version": 1,
        "metadata": {
            "timestamp": "2026-03-02T12:00:00+00:00",
            "component": {
                "type": "machine-learning-model",
                "name": "cli-test-model",
                "version": "1.0",
                "hashes": [{"alg": "SHA-256", "content": "abc" * 20}],
            },
        },
        "properties": [],
    }


def _sample_differential() -> dict:
    return {
        "run_id": "diff-cli-001",
        "base_model_id": "base-model",
        "custom_model_id": "custom-model",
        "overall_risk_delta": 0.10,
        "critical_alerts": [],
        "differential_analysis": {},
    }


def _write_json(path: Path, data: dict | list) -> None:
    path.write_text(json.dumps(data), encoding="utf-8")


# ---------------------------------------------------------------------------
# 1. Basic CLI workflow
# ---------------------------------------------------------------------------

class TestCLIBasicWorkflow:
    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    def test_basic_run_succeeds(self, mock_db, mock_render, mock_avail):
        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            output_path = Path(tmpdir) / "technical_file.pdf"
            _write_json(findings_path, [_make_finding()])

            result = runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--output", str(output_path),
            ])
            assert result.exit_code == 0, result.output

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    def test_json_artefact_created(self, mock_db, mock_render, mock_avail):
        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            output_path = Path(tmpdir) / "technical_file.pdf"
            _write_json(findings_path, [_make_finding()])

            runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--output", str(output_path),
            ])

            json_path = output_path.with_suffix(".json")
            assert json_path.exists()

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    def test_json_artefact_is_valid(self, mock_db, mock_render, mock_avail):
        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            output_path = Path(tmpdir) / "technical_file.pdf"
            _write_json(findings_path, [_make_finding()])

            runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--output", str(output_path),
            ])

            json_path = output_path.with_suffix(".json")
            data = json.loads(json_path.read_text())
            assert "sections" in data
            assert "executive_summary" in data

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    def test_output_message_in_stdout(self, mock_db, mock_render, mock_avail):
        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            output_path = Path(tmpdir) / "technical_file.pdf"
            _write_json(findings_path, [_make_finding()])

            result = runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--output", str(output_path),
            ])
            assert "Technical File" in result.output or "generated" in result.output.lower()


# ---------------------------------------------------------------------------
# 2. Missing findings file
# ---------------------------------------------------------------------------

class TestMissingFindingsFile:
    def test_missing_findings_exits_1(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            result = runner.invoke(app, [
                "technical-file",
                "--findings", str(Path(tmpdir) / "nonexistent.json"),
                "--output", str(Path(tmpdir) / "output.pdf"),
            ])
            assert result.exit_code == 1


# ---------------------------------------------------------------------------
# 3. Findings format variations
# ---------------------------------------------------------------------------

class TestFindingsFormats:
    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    def test_findings_as_run_summary_dict(self, mock_db, mock_render, mock_avail):
        """Findings embedded in a RunSummary dict (the 'results' key)."""
        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            output_path = Path(tmpdir) / "technical_file.pdf"

            run_summary = {
                "run_id": "run-cli-001",
                "model_target": "test-model",
                "results": [_make_finding(), _make_finding("bias_fairness", "pass")],
                "aibom": _sample_aibom(),
            }
            _write_json(findings_path, run_summary)

            result = runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--output", str(output_path),
            ])
            assert result.exit_code == 0, result.output

            json_path = output_path.with_suffix(".json")
            data = json.loads(json_path.read_text())
            # Both findings should be included
            assert data["executive_summary"]["total_findings"] == 2

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    def test_findings_as_raw_list(self, mock_db, mock_render, mock_avail):
        """Findings as a plain JSON list."""
        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            output_path = Path(tmpdir) / "technical_file.pdf"
            _write_json(findings_path, [_make_finding(), _make_finding("jailbreak")])

            result = runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--output", str(output_path),
            ])
            assert result.exit_code == 0, result.output


# ---------------------------------------------------------------------------
# 4. With AIBoM file
# ---------------------------------------------------------------------------

class TestWithAibom:
    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    def test_separate_aibom_file(self, mock_db, mock_render, mock_avail):
        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            aibom_path = Path(tmpdir) / "aibom.json"
            output_path = Path(tmpdir) / "technical_file.pdf"

            _write_json(findings_path, [_make_finding()])
            _write_json(aibom_path, _sample_aibom())

            result = runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--aibom", str(aibom_path),
                "--output", str(output_path),
            ])
            assert result.exit_code == 0, result.output

            json_path = output_path.with_suffix(".json")
            data = json.loads(json_path.read_text())
            assert data["model_metadata"]["name"] == "cli-test-model"


# ---------------------------------------------------------------------------
# 5. With differential report
# ---------------------------------------------------------------------------

class TestWithDifferential:
    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    def test_with_differential_report(self, mock_db, mock_render, mock_avail):
        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            diff_path = Path(tmpdir) / "diff.json"
            output_path = Path(tmpdir) / "technical_file.pdf"

            _write_json(findings_path, [_make_finding()])
            _write_json(diff_path, _sample_differential())

            result = runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--differential", str(diff_path),
                "--output", str(output_path),
            ])
            assert result.exit_code == 0, result.output

            json_path = output_path.with_suffix(".json")
            data = json.loads(json_path.read_text())
            assert data["differential_report"]["run_id"] == "diff-cli-001"


# ---------------------------------------------------------------------------
# 6. With attestation
# ---------------------------------------------------------------------------

class TestWithAttestation:
    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    @patch("sichgate.database.DatabaseManager.save_technical_file", return_value="test-id")
    @patch("sichgate.crypto_attestation.CryptoAttestation.sign_findings_report")
    def test_with_attest_flag(self, mock_sign, mock_db, mock_render, mock_avail):
        mock_sign.return_value = {
            "attestation": {
                "attestation_id": "att-cli-001",
                "algorithm": "ed25519",
                "signature": "sig==",
                "public_key_b64": "pub==",
                "payload_hash": "hash",
                "timestamp": "2026-03-02T12:00:00+00:00",
            }
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            findings_path = Path(tmpdir) / "findings.json"
            output_path = Path(tmpdir) / "technical_file.pdf"
            _write_json(findings_path, [_make_finding()])

            result = runner.invoke(app, [
                "technical-file",
                "--findings", str(findings_path),
                "--attest",
                "--output", str(output_path),
            ])
            assert result.exit_code == 0, result.output
            mock_sign.assert_called_once()
