"""Sprint-6A foundation, parity and synthetic ECharts prototype contracts."""
from __future__ import annotations

import hashlib
import importlib.util
import json
import sqlite3
import stat
import sys
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]
HEALTH = ROOT / "scripts/health"
sys.path.insert(0, str(HEALTH))

from dashboard_v5.data_provider import build_bundle  # noqa: E402
from dashboard_v5.render import render  # noqa: E402

MANDATORY_PARITY_FAMILIES = (
    "Apple-Health-Trends",
    "Laborverläufe",
    "Dokumente und PDFs",
    "Gesundheitstimeline",
    "Medikamente",
    "Health Events und Phasen",
    "Symptome",
    "Persönliche Lebensmittelverträglichkeit",
    "Low-Histamin-Experimente",
    "Mapping-Queue",
    "Datenqualität",
    "System und Legacy",
)
TARGET_MODULES = (
    "metric-catalog",
    "series-client",
    "search-command",
    "chart-controller",
    "reference-band",
    "event-lanes",
    "day-drilldown",
    "document-search",
    "accessibility-table",
)


def load_module(path: Path, name: str):
    spec = importlib.util.spec_from_file_location(name, path)
    assert spec and spec.loader
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def empty_bundle() -> dict[str, object]:
    connection = sqlite3.connect(":memory:")
    connection.row_factory = sqlite3.Row
    connection.executescript((ROOT / "database/schema.sql").read_text(encoding="utf-8"))
    try:
        return build_bundle(
            connection,
            today="2026-07-13",
            generated_at="2026-07-13T12:00:00+00:00",
        )
    finally:
        connection.close()


def test_sprint6_source_document_is_archived_and_hash_linked() -> None:
    original = ROOT / "docs/source/healthmanager-v5-repo-audit-zielbild-sprint6.docx"
    extracted = ROOT / "docs/source/healthmanager-v5-repo-audit-zielbild-sprint6.md"
    assert original.is_file() and extracted.is_file()
    digest = hashlib.sha256(original.read_bytes()).hexdigest()
    text = extracted.read_text(encoding="utf-8")
    assert digest in text
    assert "Anweisung an Jarvis – Sprint 6A" in text
    assert "Übergreifende Definition of Done" in text


def test_parity_matrix_has_one_target_and_supported_status_for_every_required_family() -> None:
    text = (ROOT / "docs/dashboard_v4_v5_parity.md").read_text(encoding="utf-8")
    rows = [line for line in text.splitlines() if line.startswith("|") and not line.startswith("| ---")]
    for family in MANDATORY_PARITY_FAMILIES:
        matching = [line for line in rows if f"| {family} |" in line]
        assert len(matching) == 1, family
        cells = [cell.strip() for cell in matching[0].strip("|").split("|")]
        assert len(cells) == 7
        assert cells[3] in {"ersetzt", "verbessert", "bewusst entfernt", "offen", "teilweise"}
        assert cells[2] and cells[2] != "—"
        assert cells[5] and cells[5] != "—"
    assert "unzugeordnet" not in text.casefold()


def test_architecture_target_names_required_modules_and_read_only_boundaries() -> None:
    text = (ROOT / "docs/dashboard_v5_architecture_target.md").read_text(encoding="utf-8")
    for module in TARGET_MODULES:
        assert f"`{module}`" in text
    assert "read-only" in text
    assert "Action Queue" in text
    assert "keine produktiven Gesundheitsdaten" in text


def test_chart_adr_records_decision_fallback_non_goals_and_local_asset_evidence() -> None:
    text = (ROOT / "docs/adr/0001-dashboard-v5-chart-engine.md").read_text(encoding="utf-8")
    for option in ("Apache ECharts 6.1.0", "Chart.js 4.5.1", "Plotly.js", "Grafana OSS"):
        assert option in text
    for phrase in (
        "Entscheidung: ECharts",
        "Feature-Flag",
        "standardmäßig aus",
        "Chart.js bleibt",
        "Rückfallstrategie",
        "Nicht-Ziele",
        "Canvas",
        "ARIA",
        "Apache-2.0",
        "CSP",
    ):
        assert phrase in text


def test_echarts_asset_is_version_pinned_hash_verified_and_locally_routed(monkeypatch, tmp_path) -> None:
    asset_dir = ROOT / "scripts/health/assets/health-assets"
    asset = asset_dir / "echarts-6.1.0.min.js"
    metadata = json.loads((asset_dir / "echarts-6.1.0.metadata.json").read_text(encoding="utf-8"))
    assert asset.is_file()
    assert metadata["version"] == "6.1.0"
    assert metadata["license"] == "Apache-2.0"
    license_file = asset_dir / metadata["license_file"]
    assert license_file.is_file()
    assert hashlib.sha256(license_file.read_bytes()).hexdigest() == metadata["license_sha256"]
    assert metadata["notice"] == "ECHARTS-6.1.0-NOTICE.txt"
    notice_file = asset_dir / metadata["notice"]
    assert notice_file.is_file()
    assert hashlib.sha256(notice_file.read_bytes()).hexdigest() == metadata["notice_sha256"]
    d3 = metadata["third_party_licenses"][0]
    assert d3["name"] == "D3" and d3["license"] == "BSD-3-Clause"
    d3_file = asset_dir / d3["file"]
    assert d3_file.is_file()
    assert hashlib.sha256(d3_file.read_bytes()).hexdigest() == d3["sha256"]
    assert metadata["sha256"] == hashlib.sha256(asset.read_bytes()).hexdigest()
    dashboard = tmp_path / "v5.html"
    dashboard.write_text("<html></html>", encoding="utf-8")
    monkeypatch.setenv("HEALTH_DASHBOARD_FILE", str(dashboard))
    monkeypatch.delenv("HEALTH_DASHBOARD_TEST_INSTANCE_ID", raising=False)
    monkeypatch.delenv("HEALTH_DASHBOARD_ACTION_INBOX", raising=False)
    server = load_module(HEALTH / "health_dashboard_server.py", "health_dashboard_server_sprint6a")
    assert server.ASSET_ROUTES["/health-assets/echarts-6.1.0.min.js"][0] == asset.name
    assert server.ASSET_ROUTES["/health-assets/dashboard-v5-echarts-prototype.js"][0] == "dashboard-v5-echarts-prototype.js"


def test_echarts_prototype_is_default_off_and_explicitly_synthetic_only(tmp_path) -> None:
    bundle = empty_bundle()
    default_page = render(bundle)
    assert "data-echarts-prototype" not in default_page
    assert "echarts-6.1.0.min.js" in default_page
    assert "dashboard-v5-echarts-prototype.js" not in default_page

    prototype_page = render(bundle, echarts_prototype=True)
    assert "data-echarts-prototype" in prototype_page
    assert "echarts-6.1.0.min.js" in prototype_page
    assert "dashboard-v5-echarts-prototype.js" in prototype_page
    assert "Zugängliche synthetische Datentabelle" in prototype_page

    generator = load_module(HEALTH / "health_dashboard_v5.py", "health_dashboard_v5_sprint6a")
    with pytest.raises(ValueError, match="must stay below /tmp"):
        generator.require_synthetic_prototype_db(ROOT / "not-synthetic.db")


def test_prototype_generator_rejects_unmarked_database_inside_tmp(tmp_path: Path) -> None:
    generator = load_module(HEALTH / "health_dashboard_v5.py", "health_dashboard_v5_marker")
    database = tmp_path / "not-a-synthetic-fixture.db"
    connection = sqlite3.connect(database)
    connection.execute("CREATE TABLE ordinary_data(value TEXT)")
    connection.close()
    with pytest.raises(ValueError, match="synthetic fixture marker"):
        generator.connect_synthetic_prototype_db(database)

    connection = sqlite3.connect(database)
    connection.execute("CREATE TABLE dashboard_v5_synthetic_fixture(marker TEXT PRIMARY KEY)")
    connection.execute(
        "INSERT INTO dashboard_v5_synthetic_fixture(marker) VALUES (?)",
        ("dashboard-v5-synthetic-fixture-v1",),
    )
    connection.commit()
    connection.close()
    assert generator.require_synthetic_prototype_db(database) == database.resolve()
    readonly = generator.connect_synthetic_prototype_db(database)
    with pytest.raises(sqlite3.OperationalError, match="readonly"):
        readonly.execute("CREATE TABLE forbidden_write(value TEXT)")
    readonly.close()

    alias = tmp_path / "database-link.db"
    alias.symlink_to(database)
    with pytest.raises(ValueError, match="must not be a symlink"):
        generator.connect_synthetic_prototype_db(alias)


def test_prototype_output_is_private_exclusive_and_ignores_tmpdir(tmp_path: Path, monkeypatch) -> None:
    generator = load_module(HEALTH / "health_dashboard_v5.py", "health_dashboard_v5_output")
    monkeypatch.setenv("TMPDIR", str(ROOT))
    with pytest.raises(ValueError, match="must stay below /tmp"):
        generator.write_synthetic_prototype_output(ROOT / "prototype.html", "synthetic")
    output = tmp_path / "prototype.html"
    assert generator.write_synthetic_prototype_output(output, "synthetic") == output.resolve()
    assert output.read_text(encoding="utf-8") == "synthetic"
    assert stat.S_IMODE(output.stat().st_mode) == 0o600
    with pytest.raises(FileExistsError, match="must not already exist"):
        generator.write_synthetic_prototype_output(output, "replacement")
    assert output.read_text(encoding="utf-8") == "synthetic"

    victim = tmp_path / "victim.html"
    victim.write_text("keep", encoding="utf-8")
    alias = tmp_path / "prototype-link.html"
    alias.symlink_to(victim)
    with pytest.raises(FileExistsError, match="must not already exist"):
        generator.write_synthetic_prototype_output(alias, "replacement")
    assert victim.read_text(encoding="utf-8") == "keep"

    real_parent = tmp_path / "real-private"
    real_parent.mkdir(mode=0o700)
    parent_alias = tmp_path / "private-parent-link"
    parent_alias.symlink_to(real_parent, target_is_directory=True)
    with pytest.raises(ValueError, match="symlink"):
        generator.write_synthetic_prototype_output(parent_alias / "prototype.html", "synthetic")


def test_prototype_output_rejects_public_or_swapped_parent(tmp_path: Path, monkeypatch) -> None:
    generator = load_module(HEALTH / "health_dashboard_v5.py", "health_dashboard_v5_parent_swap")
    public = tmp_path / "public"
    public.mkdir(mode=0o755)
    with pytest.raises(ValueError, match="mode 0700"):
        generator.write_synthetic_prototype_output(public / "prototype.html", "synthetic")

    private = tmp_path / "private"
    private.mkdir(mode=0o700)
    moved = tmp_path / "moved-private"
    victim = tmp_path / "victim-parent"
    victim.mkdir(mode=0o700)
    original = generator._inspect_private_prototype_path

    def swap_parent(path: Path, *, label: str, must_exist: bool):
        validated = original(path, label=label, must_exist=must_exist)
        private.rename(moved)
        private.mkdir(mode=0o700)
        return validated

    monkeypatch.setattr(generator, "_inspect_private_prototype_path", swap_parent)
    with pytest.raises(ValueError, match="changed after validation"):
        generator.write_synthetic_prototype_output(private / "prototype.html", "synthetic")
    assert not (victim / "prototype.html").exists()
    assert not (moved / "prototype.html").exists()


def test_prototype_source_has_fixed_scale_interactions_and_no_external_state() -> None:
    source = (ROOT / "scripts/health/assets/health-assets/dashboard-v5-echarts-prototype.js").read_text(encoding="utf-8")
    for contract in (
        "PROTOTYPE_DAYS = 365",
        "PROTOTYPE_SERIES = 8",
        "dataZoom",
        "brush",
        "axisPointer",
        "markArea",
        "markPoint",
        "health:prototype-day-select",
        "aria:",
        "solo",
    ):
        assert contract in source
    assert "fetch(" not in source
    assert "localStorage" not in source
    assert "sessionStorage" not in source
    assert "http://" not in source and "https://" not in source


def test_sprint5c_documentation_matches_deployed_release() -> None:
    text = (ROOT / "docs/sprint5c-dashboard-v5-mobile-capture.md").read_text(encoding="utf-8")
    assert "**Status:** produktiv in der parallelen v5-Preview deployt" in text
    assert "c3db3f8" in text
    assert "Abschlussreview ausstehend" not in text
    assert "Modusmigration auf `0700` bleibt" not in text
