"""
Test suite for the geney-tiles API contract: serialize_mutation(),
section descriptors, and the data-driven rendering schema.

Verifies that:
  1. serialize_mutation() produces well-formed JSON for all mechanism states
  2. Section descriptors are valid and complete
  3. The three-level hierarchy (verdict → mechanisms → details) is consistent
  4. Legacy data adaptation works correctly
  5. New mechanisms appear automatically in the output

Run with:  pytest tests/test_api_contract.py -v
"""

import json

import numpy as np
import pytest

from geney.results import (
    SplicingResult,
    ProteinResult,
    TISResult,
    FoldingResult,
    FoldingWindow,
    TranscriptionResult,
    MiRNAResult,
    MutationResult,
)
from geney.api import serialize_mutation
from geney.api_sections import (
    stats_grid, stat, alert, bar_chart, table,
    sequence_viewer, structure_viewer, text, badge_list,
    metric, divider, section_group,
)


# ── Fixtures ──

@pytest.fixture
def full_result():
    """A realistic MutationResult with all 6 mechanisms completed."""
    return MutationResult(
        splicing=SplicingResult(
            max_oncosplice_score=6.2, missplicing=0.85,
            conservation_vector=np.random.rand(300),
            mut_id="KRAS:12:25227343:G:T", gene="KRAS",
            transcript_id="ENST00000256078",
        ),
        protein=ProteinResult(
            status="completed", score=7.0, mutation_type="missense",
            mutation_description="G12V", ref_length=189, var_length=189,
            n_mismatches=1, first_mismatch=11, oncosplice_score=4.5,
            oncosplice_percentile=0.92, has_conservation_data=True,
        ),
        tis=TISResult(
            status="completed", score=0.15, canonical_prob_var=0.72,
            canonical_prob_ref=0.78, canonical_prob_change=-0.06,
            initiation_intact=True, num_alternative_tis=2,
            top_alternative_prob=0.12,
        ),
        folding=FoldingResult(
            status="completed", score=3.8, delta_mfe=-2.1,
            empirical_p=0.032, null_z_score=2.1, null_percentile=96.8,
            bp_distance=4, jaccard_similarity=0.65, null_n_samples=117,
            central_window=FoldingWindow(
                ref_sequence="AUGCUAGCUAGCUAGCUAGCUAGCUAGCUAGCUAGCUAG",
                var_sequence="AUGCUAGCUAGCUAGCUGGCUAGCUAGCUAGCUAGCUAG",
                ref_structure="((((...))))...((((...))))...............",
                var_structure="((((...))))....(((.....)))..............",
                ref_mfe=-8.5, var_mfe=-6.4, delta_mfe=2.1, mutation_offset=18,
            ),
        ),
        transcription=TranscriptionResult(
            status="completed", score=5.5,
            tissue_fold_changes={"Brain - Cortex": -0.45, "Lung": -0.32},
            max_lfc_tissue="Brain - Cortex", max_lfc_value=-0.45,
            max_abs_lfc=0.45, num_affected_tissues=8,
            predominant_direction="downregulated",
            n_upregulated=1, n_downregulated=7,
            top_affected_group="nervous_system",
        ),
        mirna=MiRNAResult(
            status="completed", score=3.5, n_sites_ref=12, n_sites_var=10,
            n_lost=2, n_gained=0, n_modified=1, repression_delta=0.6,
            n_mirnas_scanned=85, total_repression_ref=-1.8,
            total_repression_var=-1.2,
            top_affected_mirnas=[
                {"mirna_id": "hsa-miR-21-5p", "total_abs_impact": 0.35},
            ],
        ),
    )


@pytest.fixture
def empty_result():
    """A MutationResult with nothing run."""
    return MutationResult()


@pytest.fixture
def partial_result():
    """Only splicing and protein completed."""
    return MutationResult(
        splicing=SplicingResult(max_oncosplice_score=3.0,
                                conservation_vector=np.random.rand(100)),
        protein=ProteinResult(status="completed", score=2.0,
                              mutation_type="synonymous",
                              mutation_description="no amino acid change"),
    )


# ── 1. Top-level structure ──

class TestSerializeStructure:
    """serialize_mutation() produces the correct three-level hierarchy."""

    def test_has_three_levels(self, full_result):
        payload = serialize_mutation(full_result, mut_id="TEST:1:100:A:G", gene="TEST")
        assert "verdict" in payload
        assert "mechanisms" in payload
        assert "details" in payload
        assert "meta" in payload

    def test_json_serializable(self, full_result):
        payload = serialize_mutation(full_result)
        # Should not raise
        serialized = json.dumps(payload, default=str)
        assert len(serialized) > 100

    def test_empty_result_serializes(self, empty_result):
        payload = serialize_mutation(empty_result)
        assert payload["verdict"]["overall_score"] == 0.0
        assert len(payload["mechanisms"]) == 6

    def test_partial_result_serializes(self, partial_result):
        payload = serialize_mutation(partial_result)
        completed = [m for m in payload["mechanisms"] if m["status"] == "completed"]
        not_run = [m for m in payload["mechanisms"] if m["status"] == "not_run"]
        assert len(completed) >= 2  # splicing + protein at minimum
        assert len(not_run) >= 2


# ── 2. Verdict (Level 1) ──

class TestVerdict:

    def test_verdict_fields(self, full_result):
        v = serialize_mutation(full_result)["verdict"]
        assert "overall_score" in v
        assert "classification" in v
        assert "headline" in v
        assert "confidence" in v
        assert "n_concordant" in v
        assert "interaction_flags" in v

    def test_overall_score_range(self, full_result):
        v = serialize_mutation(full_result)["verdict"]
        assert 0.0 <= v["overall_score"] <= 10.0

    def test_classification_valid(self, full_result):
        v = serialize_mutation(full_result)["verdict"]
        valid = {"likely_lof", "possible_lof", "possible_gof",
                 "regulatory_effect", "translational_effect", "vus"}
        assert v["classification"] in valid

    def test_headline_nonempty(self, full_result):
        v = serialize_mutation(full_result)["verdict"]
        assert len(v["headline"]) > 0

    def test_empty_result_vus(self, empty_result):
        v = serialize_mutation(empty_result)["verdict"]
        assert v["classification"] == "vus"


# ── 3. Mechanisms (Level 2) ──

class TestMechanisms:

    def test_six_mechanisms(self, full_result):
        mechs = serialize_mutation(full_result)["mechanisms"]
        assert len(mechs) == 6

    def test_mechanism_fields(self, full_result):
        required = {"id", "label", "icon", "color", "score", "status",
                    "confidence", "summary_line"}
        for m in serialize_mutation(full_result)["mechanisms"]:
            assert required.issubset(m.keys()), f"Missing keys in {m['id']}: {required - m.keys()}"

    def test_sorted_by_score_descending(self, full_result):
        mechs = serialize_mutation(full_result)["mechanisms"]
        completed = [m for m in mechs if m["status"] == "completed"]
        scores = [m["score"] for m in completed]
        assert scores == sorted(scores, reverse=True)

    def test_summary_line_nonempty(self, full_result):
        for m in serialize_mutation(full_result)["mechanisms"]:
            if m["status"] == "completed":
                assert len(m["summary_line"]) > 0, f"{m['id']} has empty summary"

    def test_key_metric_format(self, full_result):
        for m in serialize_mutation(full_result)["mechanisms"]:
            km = m.get("key_metric")
            if km is not None:
                assert "label" in km
                assert "value" in km


# ── 4. Details (Level 3) ──

class TestDetails:

    def test_details_have_data_and_sections(self, full_result):
        details = serialize_mutation(full_result)["details"]
        for mech_id, detail in details.items():
            if mech_id == "multi_transcript":
                continue
            assert "data" in detail, f"{mech_id} missing 'data'"
            assert "sections" in detail, f"{mech_id} missing 'sections'"

    def test_sections_are_lists(self, full_result):
        details = serialize_mutation(full_result)["details"]
        for mech_id, detail in details.items():
            sections = detail.get("sections", [])
            assert isinstance(sections, list), f"{mech_id} sections not a list"

    def test_all_sections_have_type(self, full_result):
        details = serialize_mutation(full_result)["details"]
        for mech_id, detail in details.items():
            for s in detail.get("sections", []):
                assert "type" in s, f"{mech_id}: section missing 'type': {s}"

    def test_section_types_valid(self, full_result):
        valid_types = {
            "stats_grid", "alert", "bar_chart", "table", "sequence",
            "structure", "text", "badge_list", "metric", "divider",
            "section_group",
        }
        details = serialize_mutation(full_result)["details"]
        for mech_id, detail in details.items():
            for s in detail.get("sections", []):
                assert s["type"] in valid_types, f"{mech_id}: unknown section type '{s['type']}'"

    def test_each_mechanism_has_sections(self, full_result):
        details = serialize_mutation(full_result)["details"]
        for mech_id in ("protein", "tis", "folding", "transcription", "mirna"):
            assert mech_id in details, f"{mech_id} not in details"
            n = len(details[mech_id].get("sections", []))
            assert n > 0, f"{mech_id} has 0 sections"

    def test_folding_has_structure_sections(self, full_result):
        sections = serialize_mutation(full_result)["details"]["folding"]["sections"]
        types = [s["type"] for s in sections]
        assert "structure" in types

    def test_transcription_has_bar_chart(self, full_result):
        sections = serialize_mutation(full_result)["details"]["transcription"]["sections"]
        types = [s["type"] for s in sections]
        assert "bar_chart" in types


# ── 5. Section descriptor constructors ──

class TestSectionConstructors:

    def test_stats_grid_structure(self):
        s = stats_grid([stat("A", 1), stat("B", 2)], columns=2)
        assert s["type"] == "stats_grid"
        assert s["columns"] == 2
        assert len(s["items"]) == 2

    def test_alert_structure(self):
        s = alert("test message", "warning")
        assert s["type"] == "alert"
        assert s["severity"] == "warning"

    def test_bar_chart_structure(self):
        s = bar_chart("Title", [{"label": "x", "value": 1}])
        assert s["type"] == "bar_chart"
        assert len(s["data"]) == 1

    def test_table_structure(self):
        s = table("Title", [{"key": "a", "label": "A"}], [{"a": 1}])
        assert s["type"] == "table"
        assert len(s["rows"]) == 1

    def test_structure_viewer(self):
        s = structure_viewer("Ref", "AUGC", "((..))", -2.5)
        assert s["type"] == "structure"
        assert s["mfe"] == -2.5

    def test_section_group(self):
        s = section_group("Group", [alert("inner", "info")])
        assert s["type"] == "section_group"
        assert len(s["sections"]) == 1

    def test_metric_structure(self):
        s = metric("Score", 4.5, unit="", format=".2f")
        assert s["type"] == "metric"
        assert s["value"] == 4.5

    def test_badge_list_structure(self):
        s = badge_list("Tags", [{"label": "A"}, {"label": "B"}])
        assert s["type"] == "badge_list"
        assert len(s["badges"]) == 2


# ── 6. Metadata passthrough ──

class TestMeta:

    def test_meta_fields(self, full_result):
        m = serialize_mutation(full_result, mut_id="X:1:100:A:G", gene="X")["meta"]
        assert m["mut_id"] == "X:1:100:A:G"
        assert m["gene"] == "X"

    def test_timing_included(self, full_result):
        m = serialize_mutation(full_result)["meta"]
        assert "timing" in m

    def test_errors_included(self, full_result):
        m = serialize_mutation(full_result)["meta"]
        assert "errors" in m
