"""Comprehensive end-to-end integration tests for the Orxaq pipeline.

Tests the full lifecycle of credit risk operations, causal discovery,
CLI commands, data quality, learning engine, and audit trail. Each test
class covers one complete pipeline to verify cross-module integration.
"""

from __future__ import annotations

import csv
import json
import tempfile

import pytest

from orxaq.experiences.cli import main
from orxaq.intelligence.learning import (
    DiscoveryMetrics,
    ExperimentTracker,
    ImprovementDetector,
    MetricCollector,
)
from orxaq.kernel.audit import AuditLog
from orxaq.orchestration.integration import OrxaqEngine

# ---------------------------------------------------------------------------
# 1. Full Credit Pipeline E2E
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestCreditPipelineE2E:
    """Full lifecycle: ontology -> synthetic -> PD -> ECL -> stress -> fairness."""

    def setup_method(self):
        self.engine = OrxaqEngine()

    def test_full_credit_lifecycle(self):
        """Chain all credit ops and verify results cascade correctly."""
        # Step 1: Load ontology
        dag = self.engine.load_credit_ontology()
        assert dag.node_count() == 19
        assert dag.edge_count() > 0

        # Step 2: Generate synthetic data
        data = self.engine.generate_synthetic(n=500, seed=42)
        assert len(data) == 500
        assert "credit_score" in data[0]
        assert "default_flag" in data[0]

        # Step 3: Fit PD model
        model = self.engine.fit_pd_model(data)
        pd_value = model.predict_pd(data[0])
        assert 0.0 <= pd_value <= 1.0

        # Step 4: Compute ECL
        ecl_result = self.engine.compute_ecl(data)
        assert ecl_result.total_ecl >= 0
        assert ecl_result.horizon_quarters > 0
        assert ecl_result.pd_mean >= 0
        assert ecl_result.lgd > 0
        assert ecl_result.ead_total > 0
        lo, hi = ecl_result.confidence_band
        assert lo <= ecl_result.total_ecl <= hi

        # Step 5: Stress test
        stress = self.engine.stress_test(data, "severely_adverse")
        assert stress["scenario"] == "severely_adverse"
        assert stress["stress_pd"] >= stress["base_pd"]
        assert stress["delta"] >= 0

        # Step 6: Fairness audit
        decomp = self.engine.fairness_audit(protected="age")
        assert decomp.protected_var == "age"
        assert decomp.outcome_var == "default_flag"
        assert isinstance(decomp.direct_effect, float)
        assert isinstance(decomp.total_effect, float)

        # Step 7: Verify audit log grew through entire chain
        entries = self.engine.audit_log.entries()
        actions = [e.action for e in entries]
        assert "ontology_loaded" in actions
        assert "synthetic_generated" in actions
        assert "stress_test" in actions
        assert "fairness_audit" in actions
        assert len(entries) >= 6  # at least one per operation

    def test_credit_pipeline_with_csv_roundtrip(self):
        """Generate data -> write to CSV -> ingest -> fit -> compute ECL."""
        # Generate synthetic data
        self.engine.load_credit_ontology()
        data = self.engine.generate_synthetic(n=200, seed=99)
        assert len(data) == 200

        # Write to temp CSV
        with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, newline="") as f:
            columns = sorted(data[0].keys())
            writer = csv.DictWriter(f, fieldnames=columns)
            writer.writeheader()
            writer.writerows(data)
            csv_path = f.name

        # Ingest via fabric
        dataset = self.engine.ingest_csv(csv_path)
        assert dataset.row_count == 200
        assert dataset.connector_type == "csv"

        # Fit PD model on ingested data (use original data dicts)
        model = self.engine.fit_pd_model(data)
        pd_val = model.predict_pd(data[0])
        assert 0.0 <= pd_val <= 1.0

        # Compute ECL
        ecl_result = self.engine.compute_ecl(data)
        assert ecl_result.total_ecl >= 0
        assert ecl_result.horizon_quarters > 0

    def test_multi_scenario_stress(self):
        """Run all 3 scenarios and verify ordering: base < adverse < severe."""
        self.engine.load_credit_ontology()
        data = self.engine.generate_synthetic(n=500, seed=42)

        baseline = self.engine.stress_test(data, "baseline")
        adverse = self.engine.stress_test(data, "adverse")
        severe = self.engine.stress_test(data, "severely_adverse")

        # Stress PD should increase with severity
        assert baseline["stress_pd"] <= adverse["stress_pd"]
        assert adverse["stress_pd"] <= severe["stress_pd"]

        # All should have correct scenario labels
        assert baseline["scenario"] == "baseline"
        assert adverse["scenario"] == "adverse"
        assert severe["scenario"] == "severely_adverse"

    def test_credit_deterministic_with_seed(self):
        """Same seed produces identical synthetic data and PD predictions."""
        engine_a = OrxaqEngine()
        engine_a.load_credit_ontology()
        data_a = engine_a.generate_synthetic(n=100, seed=77)
        model_a = engine_a.fit_pd_model(data_a)
        pd_a = model_a.predict_pd(data_a[0])

        engine_b = OrxaqEngine()
        engine_b.load_credit_ontology()
        data_b = engine_b.generate_synthetic(n=100, seed=77)
        model_b = engine_b.fit_pd_model(data_b)
        pd_b = model_b.predict_pd(data_b[0])

        assert data_a == data_b
        assert pd_a == pd_b

    def test_ecl_confidence_band_contains_total(self):
        """ECL confidence band must bracket the total ECL value."""
        self.engine.load_credit_ontology()
        data = self.engine.generate_synthetic(n=300, seed=42)
        ecl = self.engine.compute_ecl(data)
        lo, hi = ecl.confidence_band
        assert lo <= ecl.total_ecl <= hi
        assert lo >= 0


# ---------------------------------------------------------------------------
# 2. Full Discovery Pipeline E2E
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestDiscoveryPipelineE2E:
    """Discovery, validation, and consensus pipeline tests."""

    def setup_method(self):
        self.engine = OrxaqEngine()
        self.engine.load_credit_ontology()
        self.data = self.engine.generate_synthetic(n=300, seed=42)

    def test_discover_validate_cycle(self):
        """Load ontology -> generate data -> discover (PC) -> validate edges."""
        variables = ["unemployment_rate", "income", "credit_score"]
        result = self.engine.discover_structure(self.data, variables=variables, use_consensus=False)

        # PC result has skeleton and ci_tests_run
        assert hasattr(result, "skeleton")
        assert hasattr(result, "ci_tests_run")
        assert result.ci_tests_run > 0
        # Skeleton should only have edges between the selected variables
        for a, b in result.skeleton:
            assert a in variables
            assert b in variables

    def test_consensus_vs_pc(self):
        """Run both discovery methods and compare results."""
        variables = ["unemployment_rate", "income", "credit_score"]

        pc_result = self.engine.discover_structure(
            self.data, variables=variables, use_consensus=False
        )
        consensus_result = self.engine.discover_structure(
            self.data, variables=variables, use_consensus=True
        )

        # PC returns skeleton + oriented; consensus returns edges + algorithms
        assert hasattr(pc_result, "skeleton")
        assert hasattr(consensus_result, "edges")
        assert hasattr(consensus_result, "algorithms_used")
        assert len(consensus_result.algorithms_used) >= 2

    def test_discover_with_custom_variables(self):
        """Select subset -> discover -> verify edges only connect selected vars."""
        subset = ["gdp_growth", "unemployment_rate", "fed_funds_rate"]
        result = self.engine.discover_structure(self.data, variables=subset, use_consensus=False)

        for a, b in result.skeleton:
            assert a in subset, f"Unexpected variable {a} not in subset"
            assert b in subset, f"Unexpected variable {b} not in subset"

    def test_validate_dag_post_discovery(self):
        """Discover -> validate discovered DAG -> check violation rate."""
        report = self.engine.validate_dag(self.data)

        assert report.total_implications > 0
        assert isinstance(report.violation_rate, float)
        assert 0.0 <= report.violation_rate <= 1.0
        # With synthetic data from the known DAG, violation rate should be low
        assert report.violation_rate < 0.5


# ---------------------------------------------------------------------------
# 3. CLI E2E
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestCLIE2E:
    """Test all CLI commands with --json flag via capsys."""

    def test_cli_status_json(self, capsys):
        """CLI status --json returns valid JSON with expected keys."""
        rc = main(["--json", "status"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "dag" in data
        assert "audit_entries" in data
        assert "catalog" in data

    def test_cli_credit_ontology_json(self, capsys):
        """CLI credit ontology --json returns entities and edges."""
        rc = main(["--json", "credit", "ontology"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "entities" in data
        assert "edges" in data
        assert "entity_count" in data
        assert data["entity_count"] == 19

    def test_cli_credit_pd_json(self, capsys):
        """CLI credit pd --json returns avg_pd and sample_size."""
        rc = main(["--json", "credit", "pd"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "avg_pd" in data
        assert "sample_size" in data
        assert 0.0 <= data["avg_pd"] <= 1.0

    def test_cli_credit_ecl_json(self, capsys):
        """CLI credit ecl --json returns ECL components."""
        rc = main(["--json", "credit", "ecl"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "total_ecl" in data
        assert "pd_mean" in data
        assert "lgd" in data
        assert "ead_total" in data
        assert "horizon_quarters" in data
        assert "confidence_band" in data
        assert data["total_ecl"] >= 0

    def test_cli_discover_json(self, capsys):
        """CLI discover --json returns skeleton or consensus edges."""
        rc = main(["--json", "discover"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        # Default is PC (no --consensus flag)
        assert "skeleton" in data or "edges" in data
        if "skeleton" in data:
            assert "ci_tests_run" in data
        else:
            assert "algorithms_used" in data

    def test_cli_validate_json(self, capsys):
        """CLI validate --json returns d-sep validation report."""
        rc = main(["--json", "validate"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "total_implications" in data
        assert "violations" in data
        assert "violation_rate" in data
        assert "passed" in data

    def test_cli_fabric_profile_json(self, capsys):
        """CLI fabric profile --json returns profile with columns."""
        rc = main(["--json", "fabric", "profile"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "row_count" in data
        assert "column_count" in data
        assert "columns" in data
        assert data["row_count"] > 0

    def test_cli_simulate_json(self, capsys):
        """CLI simulate --json returns simulation result."""
        rc = main(["--json", "simulate"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "scenario" in data
        assert "steps" in data
        assert "total_affected" in data
        assert "impact_score" in data

    def test_cli_audit_json(self, capsys):
        """CLI audit --json returns entries list."""
        rc = main(["--json", "audit"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "entries" in data
        assert isinstance(data["entries"], list)

    def test_cli_credit_fairness_json(self, capsys):
        """CLI credit fairness --json returns decomposition."""
        rc = main(["--json", "credit", "fairness"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "protected_var" in data
        assert "outcome_var" in data
        assert "direct_effect" in data
        assert "total_effect" in data
        assert data["protected_var"] == "age"

    def test_cli_discover_consensus_json(self, capsys):
        """CLI discover --consensus --json returns consensus edges."""
        rc = main(["--json", "discover", "--consensus"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "edges" in data
        assert "algorithms_used" in data
        assert "agreement_threshold" in data
        assert len(data["algorithms_used"]) >= 2

    def test_cli_fabric_quality_json(self, capsys):
        """CLI fabric quality --json returns gate results."""
        rc = main(["--json", "fabric", "quality"])
        assert rc == 0
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "all_passed" in data
        assert "overall_score" in data
        assert "gates" in data
        assert isinstance(data["gates"], list)

    def test_cli_no_command_returns_1(self, capsys):
        """CLI with no command returns exit code 1."""
        rc = main([])
        assert rc == 1


# ---------------------------------------------------------------------------
# 4. Data Quality E2E
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestDataQualityE2E:
    """Data ingestion, profiling, and quality gate pipeline tests."""

    def setup_method(self):
        self.engine = OrxaqEngine()

    def test_ingest_profile_quality_chain(self):
        """Ingest CSV -> profile -> quality gates -> verify pass."""
        # Create clean CSV data
        with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, newline="") as f:
            writer = csv.writer(f)
            writer.writerow(["a", "b", "c"])
            for i in range(50):
                writer.writerow([i, i * 2, i * 3])
            csv_path = f.name

        # Ingest
        dataset = self.engine.ingest_csv(csv_path)
        assert dataset.row_count == 50
        assert dataset.connector_type == "csv"

        # Profile
        data = list(dataset.data)
        profile = self.engine.profile_data(data)
        assert profile.row_count == 50
        assert profile.column_count == 3

        # Quality gates on clean data
        result = self.engine.run_quality_gates(data)
        assert result.all_passed is True

    def test_quality_gates_reject_bad_data(self):
        """Create data with all nulls -> quality gates must fail."""
        bad_data = [
            {"x": None, "y": None, "z": None},
            {"x": None, "y": None, "z": None},
            {"x": None, "y": None, "z": None},
        ]
        result = self.engine.run_quality_gates(bad_data)
        assert result.all_passed is False

        # Verify at least one gate reports failures
        has_failures = any(len(gr.failures) > 0 for gr in result.gate_results)
        assert has_failures

    def test_synthetic_data_quality(self):
        """Generate synthetic data -> run quality gates -> all pass."""
        self.engine.load_credit_ontology()
        data = self.engine.generate_synthetic(n=100, seed=42)

        # Profile first
        profile = self.engine.profile_data(data)
        assert profile.row_count == 100
        assert profile.column_count > 5

        # Quality gates on synthetic data
        result = self.engine.run_quality_gates(data)
        assert result.all_passed is True
        assert result.overall_score > 0.8


# ---------------------------------------------------------------------------
# 5. Learning Engine E2E
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestLearningE2E:
    """MetricCollector, ExperimentTracker, and ImprovementDetector tests."""

    def test_metric_collection_and_comparison(self):
        """Record 3 runs -> compare -> verify improvement detected."""
        collector = MetricCollector()

        # Run 1: poor quality
        run1 = DiscoveryMetrics(
            run_id="run-001",
            algorithm="pc",
            dataset_size=100,
            num_variables=5,
            shd=10,
            edge_precision=0.3,
            edge_recall=0.4,
            edge_f1=0.34,
            orientation_accuracy=0.2,
            execution_time_ms=500.0,
        )
        collector.record(run1)

        # Run 2: medium quality
        run2 = DiscoveryMetrics(
            run_id="run-002",
            algorithm="pc",
            dataset_size=500,
            num_variables=5,
            shd=5,
            edge_precision=0.6,
            edge_recall=0.7,
            edge_f1=0.65,
            orientation_accuracy=0.5,
            execution_time_ms=400.0,
        )
        collector.record(run2)

        # Run 3: good quality
        run3 = DiscoveryMetrics(
            run_id="run-003",
            algorithm="consensus",
            dataset_size=1000,
            num_variables=5,
            shd=2,
            edge_precision=0.85,
            edge_recall=0.90,
            edge_f1=0.87,
            orientation_accuracy=0.8,
            execution_time_ms=300.0,
        )
        collector.record(run3)

        # Verify collection
        assert len(collector.runs) == 3
        assert collector.latest(1)[0].run_id == "run-003"
        assert len(collector.by_algorithm("pc")) == 2
        assert len(collector.by_algorithm("consensus")) == 1

        # Best run by F1
        best = collector.best_run("edge_f1")
        assert best is not None
        assert best.run_id == "run-003"

        # Summary
        summary = collector.summary()
        assert summary["total_runs"] == 3
        assert summary["best_shd"] == 2
        assert summary["best_f1"] == 0.87

        # Improvement detection
        detector = ImprovementDetector()
        comparisons = detector.compare(run1, run3)
        assert len(comparisons) > 0

        # Overall: run3 is better than run1
        improved_metrics = [c for c in comparisons if c.improved]
        assert len(improved_metrics) >= 3  # F1, precision, recall all improved

        # No regressions from run1 to run3
        assert not detector.has_regression(run1, run3)

        # Summary check
        imp_summary = detector.improvement_summary(run1, run3)
        assert imp_summary["has_regression"] is False
        assert imp_summary["net_improvements"] > 0

    def test_experiment_lifecycle(self):
        """Create -> start -> complete -> verify summary."""
        tracker = ExperimentTracker()

        # Create experiment
        exp = tracker.create(
            experiment_id="exp-001",
            name="PC Discovery Test",
            algorithm="pc",
            algorithm_config={"alpha": 0.05, "max_cond_size": 3},
            data_config={"n": 1000, "seed": 42},
            tags=["credit", "pc"],
        )
        assert exp.status == "pending"
        assert exp.experiment_id == "exp-001"

        # Start
        started = tracker.start("exp-001")
        assert started.status == "running"

        # Complete with results
        completed = tracker.complete(
            "exp-001",
            results={"shd": 3, "f1": 0.85, "edges_found": 12},
        )
        assert completed.status == "completed"
        assert completed.results["f1"] == 0.85
        assert completed.duration_ms >= 0

        # Verify retrieval
        retrieved = tracker.get("exp-001")
        assert retrieved is not None
        assert retrieved.status == "completed"
        assert retrieved.algorithm == "pc"

        # List experiments
        all_exps = tracker.list_experiments()
        assert len(all_exps) == 1

        by_status = tracker.list_experiments(status="completed")
        assert len(by_status) == 1

        by_tag = tracker.list_experiments(tag="credit")
        assert len(by_tag) == 1

        # Summary
        summary = tracker.summary()
        assert summary["total"] == 1
        assert summary["by_status"]["completed"] == 1
        assert summary["by_algorithm"]["pc"] == 1

    def test_learning_persistence_roundtrip(self, tmp_path):
        """Record metrics -> persist to temp JSON -> load in new collector."""
        storage_path = tmp_path / "metrics.json"

        # Create collector with persistence
        collector = MetricCollector(storage_path=storage_path)

        run1 = DiscoveryMetrics(
            run_id="persist-001",
            algorithm="pc",
            dataset_size=200,
            num_variables=5,
            shd=4,
            edge_precision=0.7,
            edge_recall=0.8,
            edge_f1=0.74,
            orientation_accuracy=0.6,
            execution_time_ms=350.0,
            config={"alpha": 0.05},
        )
        collector.record(run1)

        run2 = DiscoveryMetrics(
            run_id="persist-002",
            algorithm="consensus",
            dataset_size=500,
            num_variables=5,
            shd=2,
            edge_precision=0.9,
            edge_recall=0.85,
            edge_f1=0.87,
            orientation_accuracy=0.75,
            execution_time_ms=280.0,
            config={"threshold": 0.66},
        )
        collector.record(run2)

        # Verify file was written
        assert storage_path.exists()
        raw = json.loads(storage_path.read_text())
        assert len(raw) == 2

        # Load into a new collector
        new_collector = MetricCollector()
        new_collector.load(storage_path)

        assert len(new_collector.runs) == 2
        assert new_collector.runs[0].run_id == "persist-001"
        assert new_collector.runs[1].run_id == "persist-002"
        assert new_collector.runs[0].config == {"alpha": 0.05}
        assert new_collector.runs[1].edge_f1 == 0.87

    def test_experiment_failure_lifecycle(self):
        """Create -> start -> fail -> verify status and error."""
        tracker = ExperimentTracker()
        tracker.create(
            experiment_id="exp-fail",
            name="Failing Experiment",
            algorithm="pc",
        )
        tracker.start("exp-fail")
        failed = tracker.fail("exp-fail", error="insufficient data")

        assert failed.status == "failed"
        assert failed.results["error"] == "insufficient data"
        assert failed.duration_ms >= 0

        # Summary reflects failure
        summary = tracker.summary()
        assert summary["by_status"]["failed"] == 1

    def test_improvement_regression_detection(self):
        """Detect regression when metrics worsen between runs."""
        good_run = DiscoveryMetrics(
            run_id="good",
            algorithm="pc",
            shd=2,
            edge_precision=0.9,
            edge_recall=0.85,
            edge_f1=0.87,
            orientation_accuracy=0.8,
            execution_time_ms=200.0,
        )
        bad_run = DiscoveryMetrics(
            run_id="bad",
            algorithm="pc",
            shd=10,
            edge_precision=0.3,
            edge_recall=0.4,
            edge_f1=0.34,
            orientation_accuracy=0.2,
            execution_time_ms=600.0,
        )

        detector = ImprovementDetector()
        assert detector.has_regression(good_run, bad_run) is True

        summary = detector.improvement_summary(good_run, bad_run)
        assert summary["has_regression"] is True
        assert len(summary["regressed"]) > 0
        assert summary["net_improvements"] < 0


# ---------------------------------------------------------------------------
# 6. Audit Trail E2E
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestAuditTrailE2E:
    """Hash-chained audit log integrity and filtering tests."""

    def test_audit_log_chain_integrity(self):
        """Run full pipeline -> verify audit log has correct chain + actions."""
        engine = OrxaqEngine()
        engine.load_credit_ontology()
        data = engine.generate_synthetic(n=100, seed=42)
        engine.fit_pd_model(data)
        engine.compute_ecl(data)
        engine.stress_test(data, "adverse")
        engine.fairness_audit(protected="age")

        # Verify chain integrity
        assert engine.audit_log.verify() is True

        # Verify correct sequence of actions
        entries = engine.audit_log.entries()
        actions = [e.action for e in entries]
        assert "ontology_loaded" in actions
        assert "synthetic_generated" in actions
        assert "pd_model_fit" in actions
        assert "ecl_computed" in actions
        assert "stress_test" in actions
        assert "fairness_audit" in actions

        # Verify hash chain linkage
        for i in range(1, len(entries)):
            assert entries[i].prev_hash == entries[i - 1].entry_hash
        assert entries[0].prev_hash == "genesis"

        # Verify all entries have non-empty hashes
        for entry in entries:
            assert entry.entry_hash != ""
            assert len(entry.entry_hash) == 64  # SHA-256 hex length

    def test_audit_log_filters_by_action(self):
        """Multiple actions -> filter -> correct subset returned."""
        audit = AuditLog()

        # Record multiple actions
        audit.append(action="ingest", actor="fabric", details="csv loaded")
        audit.append(action="profile", actor="fabric", details="profiled 100 rows")
        audit.append(action="ingest", actor="fabric", details="json loaded")
        audit.append(action="quality_check", actor="fabric", details="gates passed")
        audit.append(action="ingest", actor="api", details="api upload")

        # Filter by action
        ingest_entries = audit.search(action="ingest")
        assert len(ingest_entries) == 3

        profile_entries = audit.search(action="profile")
        assert len(profile_entries) == 1
        assert profile_entries[0].details == "profiled 100 rows"

        # Filter by actor
        fabric_entries = audit.search(actor="fabric")
        assert len(fabric_entries) == 4

        api_entries = audit.search(actor="api")
        assert len(api_entries) == 1

        # Compound filter
        fabric_ingest = audit.search(action="ingest", actor="fabric")
        assert len(fabric_ingest) == 2

        # Verify chain integrity even after searches (no mutation)
        assert audit.verify() is True


# ---------------------------------------------------------------------------
# 8. Full Filing Workflow E2E
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestFilingE2E:
    """End-to-end tests for the complete regulatory filing pipeline.

    Verifies that FilingGenerator composes all credit modules correctly
    and produces valid, schema-conformant regulatory artifacts.
    """

    @pytest.fixture(scope="class")
    def filing_dir(self, tmp_path_factory):
        """Generate a full filing package once; share across all class tests."""
        from orxaq.credit.filing import FilingConfig, FilingGenerator

        out = tmp_path_factory.mktemp("e2e_filing")
        cfg = FilingConfig(
            institution_id="E2E_BANK",
            period="2026Q1",
            output_dir=str(out),
            seed=42,
            n_samples=150,
            horizon_quarters=9,
            institution_name="E2E Test Bank, N.A.",
        )
        FilingGenerator(cfg).generate()
        return out

    def test_filing_produces_eight_output_files(self, filing_dir) -> None:
        """All 8 canonical output files must be present."""
        from orxaq.credit.filing import FILING_OUTPUT_FILES

        for fname in FILING_OUTPUT_FILES:
            assert (filing_dir / fname).exists(), f"Missing output file: {fname}"

    def test_sha256_manifest_integrity(self, filing_dir) -> None:
        """SHA-256 checksums in manifest.json must match re-computed hashes."""
        import hashlib

        manifest = json.loads((filing_dir / "manifest.json").read_text())
        for fname, meta in manifest["files"].items():
            fpath = filing_dir / fname
            if fpath.exists():
                actual = hashlib.sha256(fpath.read_bytes()).hexdigest()
                assert actual == meta["sha256"], f"Checksum mismatch: {fname}"

    def test_cecl_disclosure_schema(self, filing_dir) -> None:
        """cecl_disclosure.json must have required regulatory fields."""
        data = json.loads((filing_dir / "cecl_disclosure.json").read_text())
        assert isinstance(data, dict)
        assert "framework" in data
        assert "CECL" in data["framework"] or "ASC" in data["framework"]
        assert "institution_id" in data
        assert "period" in data

    def test_ccar_export_schema(self, filing_dir) -> None:
        """ccar_export.json must have required CCAR/DFAST fields."""
        data = json.loads((filing_dir / "ccar_export.json").read_text())
        assert isinstance(data, dict)
        assert "framework" in data
        assert "CCAR" in data["framework"] or "DFAST" in data["framework"]
        assert "institution_id" in data

    def test_schedule_h1_has_rows(self, filing_dir) -> None:
        """schedule_h1.csv must be non-empty with required columns."""
        text = (filing_dir / "schedule_h1.csv").read_text()
        lines = [ln for ln in text.splitlines() if ln.strip()]
        assert len(lines) >= 2, "schedule_h1.csv must have header + data rows"
        header = lines[0].lower()
        assert any(
            col in header for col in ("quarter", "projected", "loss", "segment", "ecl", "ead")
        )

    def test_scenario_comparison_has_fed_scenarios(self, filing_dir) -> None:
        """scenario_comparison.csv must include at least one FED scenario name."""
        text = (filing_dir / "scenario_comparison.csv").read_text()
        assert len(text.strip()) > 0
        assert any(s in text for s in ("baseline", "adverse", "severely_adverse"))

    def test_audit_trail_records_all_steps(self, filing_dir) -> None:
        """audit_trail.json must record the filing start and completion actions."""
        entries = json.loads((filing_dir / "audit_trail.json").read_text())
        assert isinstance(entries, list)
        assert len(entries) >= 5
        actions = {e["action"] for e in entries}
        assert "filing_started" in actions
        assert "filing_completed" in actions
        for entry in entries:
            assert "action" in entry
            assert "timestamp" in entry

    def test_html_report_is_valid_html(self, filing_dir) -> None:
        """regulatory_report.html must be non-empty and well-structured."""
        html = (filing_dir / "regulatory_report.html").read_text()
        assert len(html) > 100
        assert "<" in html and ">" in html

    def test_markdown_report_nonempty(self, filing_dir) -> None:
        """regulatory_report.md must be non-empty."""
        md = (filing_dir / "regulatory_report.md").read_text()
        assert len(md) > 0

    def test_manifest_has_all_files_listed(self, filing_dir) -> None:
        """manifest.json must list entries for all non-manifest output files."""
        from orxaq.credit.filing import FILING_OUTPUT_FILES

        manifest = json.loads((filing_dir / "manifest.json").read_text())
        expected = [f for f in FILING_OUTPUT_FILES if f != "manifest.json"]
        for fname in expected:
            if (filing_dir / fname).exists():
                assert fname in manifest["files"], f"{fname} missing from manifest"

    def test_institution_id_propagates_through_pipeline(self, filing_dir) -> None:
        """institution_id=E2E_BANK must appear in key output files."""
        manifest = json.loads((filing_dir / "manifest.json").read_text())
        assert manifest["institution_id"] == "E2E_BANK"

        cecl = json.loads((filing_dir / "cecl_disclosure.json").read_text())
        assert cecl.get("institution_id") == "E2E_BANK"

    def test_full_pipeline_from_scratch(self, tmp_path) -> None:
        """Run a complete filing pipeline from scratch and verify integrity."""
        import hashlib

        from orxaq.credit.filing import FILING_OUTPUT_FILES, FilingConfig, FilingGenerator

        cfg = FilingConfig(
            institution_id="SCRATCH_BANK",
            period="2026Q2",
            output_dir=str(tmp_path),
            seed=99,
            n_samples=100,
            horizon_quarters=4,
        )
        result = FilingGenerator(cfg).generate()

        assert result.institution_id == "SCRATCH_BANK"
        assert result.period == "2026Q2"
        assert result.duration_seconds >= 0
        assert result.audit_entries_count >= 5
        assert set(result.files) == set(FILING_OUTPUT_FILES)

        for fname in FILING_OUTPUT_FILES:
            assert (tmp_path / fname).exists(), f"Missing: {fname}"

        manifest = json.loads((tmp_path / "manifest.json").read_text())
        for fname, meta in manifest["files"].items():
            fpath = tmp_path / fname
            if fpath.exists():
                actual = hashlib.sha256(fpath.read_bytes()).hexdigest()
                assert actual == meta["sha256"]
