from __future__ import annotations

import json
import math
from pathlib import Path

import pandas as pd
import pytest
from bs4 import BeautifulSoup

import build_explorer_site as site


EXPECTED_STUDIES = {
    "henley-shelton-2013",
    "ponti-efeds-2023",
    "ueda-2022",
    "kaaret-halosat-2020",
    "locatelli-2024",
    "zhang-2024",
    "grayson-2025",
}

EXPECTED_POINTS = {
    "henley_shelton2013_high_latitude_population",
    "ponti2023_efeds_negligible_swcx",
    "ponti2023_efeds_high_swcx",
    "ueda2022_suzaku_disk_m31_footprint",
    "kaaret2020_halosat_disk_halo_m31_extrapolation",
    "locatelli2024_reference_beta0p5_m31_footprint",
    "zhang2024_m31_mass_nominal_20kpc",
    "grayson2025_eagle_agndt9",
    "grayson2025_eagle",
    "grayson2025_eagle_noagn",
    "grayson2025_simba",
    "grayson2025_simba_noagn",
}

REQUIRED_BILINGUAL_FIELDS = {
    "overview",
    "instrument",
    "sightlines",
    "measurement",
    "model",
    "published_results",
    "transfer_to_m31",
    "limitations",
}


def assert_bilingual(value: object) -> None:
    assert isinstance(value, dict)
    assert set(value) == {"zh", "en"}
    assert all(isinstance(value[language], str) and value[language].strip() for language in ("zh", "en"))


def test_registry_covers_every_figure3_study_and_ledger_point() -> None:
    registry = site.load_reference_registry()
    assert {study["study_id"] for study in registry["studies"]} == EXPECTED_STUDIES
    assert {point["prior_id"] for point in registry["points"]} == EXPECTED_POINTS

    ledger = pd.read_csv(site.PAPER_ROOT / "m31_cgmsum_conditional_prior_ledger.csv")
    assert set(ledger["prior_id"]) == EXPECTED_POINTS
    points = {point["prior_id"]: point for point in registry["points"]}
    for row in ledger.itertuples(index=False):
        point = points[row.prior_id]
        assert point["doi"] == row.doi
        assert len(point["transfer_steps"]) >= 3
        for field in ("figure_central", "figure_low", "figure_high"):
            assert math.isclose(point[field], getattr(row, field), rel_tol=0.0, abs_tol=1.0e-12)

    for study in registry["studies"]:
        assert REQUIRED_BILINGUAL_FIELDS <= study.keys()
        for field in REQUIRED_BILINGUAL_FIELDS:
            assert_bilingual(study[field])
        citation = study["citation"]
        assert citation["doi"].startswith("10.")
        assert citation["title"]
        assert citation["journal"]
        assert citation["primary_urls"]
        assert all(url.startswith("https://") for url in citation["primary_urls"].values())
        assert len(citation["source_snapshot"]["sha256"]) == 64
        source_version = citation["source_snapshot"]["version"].removeprefix("arXiv:")
        assert citation["primary_urls"]["arxiv"].endswith(source_version)
        assert len(study["evidence"]) >= 3
        assert all(item["location"] and item["url"].startswith("https://") for item in study["evidence"])
        for item in study["evidence"]:
            if "arxiv.org/abs/" in item["url"]:
                assert item["url"].endswith(source_version)


def test_registry_validation_rejects_missing_point_and_numeric_drift() -> None:
    registry = site.load_reference_registry()
    missing = {**registry, "points": registry["points"][:-1]}
    with pytest.raises(ValueError, match="point inventory"):
        site.validate_reference_registry(missing)

    drifted = site.load_reference_registry()
    drifted["points"][0]["figure_central"] += 0.01
    with pytest.raises(ValueError, match="ledger mismatch"):
        site.validate_reference_registry(drifted)


def test_rendered_site_has_seven_study_and_twelve_point_routes(tmp_path: Path) -> None:
    target = tmp_path / "rendered"
    site.render_site(target)
    site.validate_site(target)
    registry = site.load_reference_registry()
    index = BeautifulSoup((target / "index.html").read_text(encoding="utf-8"), "html.parser")

    assert len(index.select("#figure3-reference-library .study-card")) == 7
    assert len(index.select("#figure3-reference-library .point-link")) == 12

    for study in registry["studies"]:
        route = target / "studies" / study["slug"] / "index.html"
        assert route.is_file()
        soup = BeautifulSoup(route.read_text(encoding="utf-8"), "html.parser")
        assert soup.html is not None and soup.html.get("data-page-kind") == "study"
        assert soup.select_one('[data-section="instrument"]')
        assert soup.select_one('[data-section="sightlines"]')
        assert soup.select_one('[data-section="measurement"]')
        assert soup.select_one('[data-section="model"]')
        assert soup.select_one('[data-section="transfer"]')
        assert soup.select_one('[data-section="evidence"]')
        if study["kind"] == "simulation":
            assert soup.select_one('[data-section="mass-radius-scaling"]')
        assert soup.select_one(f'a[href="https://doi.org/{study["citation"]["doi"]}"]')
        assert study["citation"]["source_snapshot"]["sha256"] in soup.get_text(" ")
        canonical = soup.select_one('link[rel="canonical"]')
        assert canonical is not None
        assert canonical["href"] == (
            f"https://m31cgm-mw-m31-profile-explorer.pages.dev/studies/{study['slug']}/"
        )
        assert len(soup.select("[data-zh]")) >= 10
        assert len(soup.select("[data-en]")) >= 10
        assert soup.html.get("data-title-zh")
        assert soup.html.get("data-title-en")

    for point in registry["points"]:
        route = target / "points" / point["slug"] / "index.html"
        assert route.is_file()
        soup = BeautifulSoup(route.read_text(encoding="utf-8"), "html.parser")
        assert soup.html is not None and soup.html.get("data-page-kind") == "point"
        assert soup.select_one('[data-section="native-input"]')
        assert soup.select_one('[data-section="conversion-chain"]')
        assert soup.select_one('[data-section="assumptions"]')
        assert soup.select_one('[data-section="figure3-result"]')
        assert len(soup.select('[data-section="conversion-chain"] li')) >= 3
        assert point["prior_id"] in soup.get_text(" ")
        assert soup.select_one('a[href^="../../studies/"]')
        canonical = soup.select_one('link[rel="canonical"]')
        assert canonical is not None
        assert canonical["href"] == (
            f"https://m31cgm-mw-m31-profile-explorer.pages.dev/points/{point['slug']}/"
        )
        assert index.select_one(f'a[href="points/{point["slug"]}/index.html"]')


def test_locatelli_page_uses_the_actual_all_field_plot_marker(tmp_path: Path) -> None:
    target = tmp_path / "rendered"
    site.render_site(target)
    registry = site.load_reference_registry()
    point = next(
        point
        for point in registry["points"]
        if point["prior_id"] == "locatelli2024_reference_beta0p5_m31_footprint"
    )
    assert math.isclose(point["plotted_central"], 1.290382023305802, abs_tol=1.0e-12)
    assert not math.isclose(point["plotted_central"], point["figure_central"])

    route = target / "points" / point["slug"] / "index.html"
    soup = BeautifulSoup(route.read_text(encoding="utf-8"), "html.parser")
    metric = soup.select_one(".result-metric")
    note = soup.select_one(".metric-note")
    assert metric is not None and "1.29038202" in metric.get_text(" ")
    assert note is not None and "1.25450068" in note.get_text(" ")
    index = BeautifulSoup((target / "index.html").read_text(encoding="utf-8"), "html.parser")
    link = index.select_one(f'a[href="points/{point["slug"]}/index.html"]')
    assert link is not None and "1.29038" in link.get_text(" ")


def test_primary_source_audit_boundaries_are_rendered(tmp_path: Path) -> None:
    registry = site.load_reference_registry()
    studies = {study["study_id"]: study for study in registry["studies"]}

    ueda = " ".join(
        str(value)
        for field in ("sightlines", "model", "published_results", "limitations")
        for value in studies["ueda-2022"][field].values()
    )
    assert "25" in ueda
    assert "N=36" in ueda
    assert "disk-only" in ueda
    assert "composite" in ueda

    locatelli = " ".join(
        str(value)
        for field in ("model", "published_results", "limitations")
        for value in studies["locatelli-2024"][field].values()
    )
    assert "Eq. (7)" in locatelli
    assert "path-length s" in locatelli
    assert "0.26" in locatelli
    assert "0.23" in locatelli

    grayson = " ".join(
        str(value)
        for field in ("instrument", "measurement", "limitations")
        for value in studies["grayson-2025"][field].values()
    )
    assert "event-energy cut" in grayson
    assert "confidence level is not stated" in grayson
    assert "not a 95% confidence interval" in grayson
    assert "M31/XMM" in grayson

    site.render_site(tmp_path)
    for slug, required in (
        (studies["ueda-2022"]["slug"], "disk-only"),
        (studies["locatelli-2024"]["slug"], "path-length s"),
        (studies["grayson-2025"]["slug"], "confidence level is not stated"),
    ):
        page = (tmp_path / "studies" / slug / "index.html").read_text(
            encoding="utf-8"
        )
        assert required in page


def test_nested_reference_routes_are_manifested_and_link_closed(tmp_path: Path) -> None:
    target = tmp_path / "rendered"
    site.render_site(target)
    site.validate_site(target)
    registry = site.load_reference_registry()
    manifest = json.loads((target / "report_manifest.json").read_text(encoding="utf-8"))
    paths = {item["path"] for item in manifest["files"]}
    expected = {
        *(f"studies/{study['slug']}/index.html" for study in registry["studies"]),
        *(f"points/{point['slug']}/index.html" for point in registry["points"]),
    }
    assert expected <= paths
