from __future__ import annotations

import importlib.util
import json
import sqlite3
import sys
from datetime import date
from pathlib import Path
from urllib.parse import urlencode

import pytest

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

from dashboard_v5 import read_api  # noqa: E402
from dashboard_v5.read_api import APIError, dispatch_api  # noqa: E402
from dashboard_v5.render import render  # noqa: E402

FIXTURE = ROOT / "tests" / "fixtures" / "dashboard_v5_fixture.py"
loader = importlib.util.spec_from_file_location("dashboard_v5_fixture_s6d", FIXTURE)
assert loader and loader.loader
fixture_module = importlib.util.module_from_spec(loader)
loader.loader.exec_module(fixture_module)
build_fixture = fixture_module.build_dashboard_v5_fixture

TODAY = date(2026, 6, 15)
CURRENT = "2026-06-14"
INCOMPLETE = "2026-06-06"
STALE = "2026-05-01"


def database(tmp_path: Path) -> Path:
    path = tmp_path / "s6d.db"
    build_fixture(path, anchor_date=TODAY)
    return path


def insert_contract_rows(path: Path) -> None:
    connection = sqlite3.connect(path)
    try:
        connection.execute(
            "INSERT INTO health_event_periods(start_date,end_date,event_type,label,source,notes) VALUES(?,?,?,?,?,?)",
            ("2026-06-10", "2026-06-16", "synthetic_phase", "SYNTHETIC_MULTI_DAY_PHASE", "fixture", "bounded synthetic phase"),
        )
        connection.executemany(
            """INSERT INTO medication_administrations
               (datum,medication_name,dose,route,event_type,notes,source) VALUES(?,?,?,?,?,?,?)""",
            [
                (CURRENT, "SYNTHETIC_MISSED", "0 mg", "synthetic", "missed", "explicit missed", "fixture"),
                (CURRENT, "SYNTHETIC_CORRECTED", "0 mg", "synthetic", "corrected", "explicit correction", "fixture"),
            ],
        )
        connection.execute(
            "INSERT INTO arztbesuche(datum,arzt,klinik,grund,notizen) VALUES(?,?,?,?,?)",
            ("2026-06-20", "Synthetic Clinician", "Synthetic Clinic", "Synthetic follow-up", "safe synthetic note"),
        )
        connection.execute(
            """INSERT INTO dokumente(datei_name,dateipfad,daten_typ,status,review_status,document_date,kategorie,institution,drive_file_id,drive_web_url,local_original_path)
               VALUES(?,?,?,?,?,?,?,?,?,?,?)""",
            ("private.pdf", "/private/secret.pdf", "pdf", "eingearbeitet", "geprueft", CURRENT, "Befund", "Synthetic Clinic", "drive-secret-identifier-123456789", "https://secret.invalid", "/private/original.pdf"),
        )
        document_id = connection.execute("SELECT last_insert_rowid()").fetchone()[0]
        connection.execute(
            """INSERT INTO laborwerte(dokument_id,canonical_document_id,parameter_name,wert,einheit,reference_min,reference_max,
               abnahme_datum,validierungsstatus,source_type,reference_range_source,verified_against_original)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
            (document_id, document_id, "C-Reaktives Protein (CRP)", "0.0", "mg/L", None, None, CURRENT, "validiert", "scanned_original", "scanned_original", 1),
        )
        connection.commit()
    finally:
        connection.close()


def insert_apple_record(path: Path, *, metric: str, start_date: str, value: float, unit: str, key: str) -> None:
    connection = sqlite3.connect(path)
    try:
        connection.execute(
            """INSERT INTO apple_health_records
               (record_type,metric,start_date,end_date,value,value_text,unit,source_name,
                source_version,device,file_name,file_hash,record_hash,raw_json,imported_at)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
            (
                "HKQuantityType", metric, start_date, start_date, value, None, unit,
                "Synthetic 6D.1 Source", "1", "Synthetic Device", f"{key}.json",
                f"{key}-file", f"{key}-record", "{}", start_date,
            ),
        )
        connection.commit()
    finally:
        connection.close()


def test_day_v2_regular_day_preserves_zero_and_complete_symptoms(tmp_path, monkeypatch):
    path = database(tmp_path)
    insert_contract_rows(path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    payload = dispatch_api(path, f"/api/v1/day/{CURRENT}", "")

    assert payload["version"] == 2
    assert payload["date"] == CURRENT
    assert payload["today"] == TODAY.isoformat()
    assert payload["timezone"] == "Europe/Zurich"
    assert payload["is_future"] is False
    assert payload["symptoms"]["complete"] is True
    assert any(item["value"] == 0 for item in payload["symptoms"]["dimensions"])
    assert payload["symptoms"]["total"] is not None
    assert payload["measurements"]
    assert all({"value", "unit", "source", "aggregation", "quality"} <= set(item) for item in payload["measurements"])


def test_day_without_data_and_incomplete_symptoms_remain_explicit(tmp_path, monkeypatch):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    empty = dispatch_api(path, "/api/v1/day/2024-02-29", "")
    incomplete = dispatch_api(path, f"/api/v1/day/{INCOMPLETE}", "")

    assert empty["coverage"]["status"] == "not_documented"
    assert empty["symptoms"]["total"] is None
    assert incomplete["symptoms"]["complete"] is False
    assert incomplete["symptoms"]["documented_dimensions"] == 6
    assert incomplete["symptoms"]["total"] is None


def test_user_reported_symptoms_and_long_note_events_remain_visible(tmp_path, monkeypatch):
    path = database(tmp_path)
    connection = sqlite3.connect(path)
    try:
        connection.execute(
            "INSERT INTO symptom_log(datum,symptom,schwergrad,kontext,notizen) VALUES(?,?,?,?,?)",
            (CURRENT, "SYNTHETIC_REPORTED_THROAT", "leicht", '{"source":"telegram_user_report"}', "synthetic reported symptom"),
        )
        connection.execute(
            "INSERT INTO health_events(date,category,parameter,value,unit,source,notes) VALUES(?,?,?,?,?,?,?)",
            (CURRENT, "SYMPTOM", "SYNTHETIC_LONG_NOTE_EVENT", "present", None, "telegram_user_report", "x" * 196),
        )
        connection.commit()
    finally:
        connection.close()
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)

    day = dispatch_api(path, f"/api/v1/day/{CURRENT}", "")
    calendar = dispatch_api(path, "/api/v1/calendar", f"from={CURRENT}&to={CURRENT}")

    assert any(item["name"] == "SYNTHETIC_REPORTED_THROAT" for item in day["symptoms"]["additional"])
    assert any(item["label"] == "SYNTHETIC_LONG_NOTE_EVENT" for item in day["events"]["events"])
    assert calendar["days"][0]["counts"]["symptoms"] >= 1
    assert calendar["days"][0]["counts"]["events"] >= 1


def test_medication_states_are_never_merged(tmp_path, monkeypatch):
    path = database(tmp_path)
    insert_contract_rows(path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    current = dispatch_api(path, f"/api/v1/day/{CURRENT}", "")
    future = dispatch_api(path, "/api/v1/day/2026-06-16", "")

    assert {item["name"] for item in current["medications"]["administered"]} == {"SYNTHETIC_ADMINISTERED_MEDICATION"}
    assert {item["name"] for item in current["medications"]["missed"]} == {"SYNTHETIC_MISSED"}
    assert {item["name"] for item in current["medications"]["corrected"]} == {"SYNTHETIC_CORRECTED"}
    assert {item["name"] for item in future["medications"]["planned"]} == {"SYNTHETIC_PLANNED_MEDICATION_WITH_A_VERY_LONG_LABEL_FOR_RESPONSIVE_LAYOUT"}
    assert future["medications"]["administered"] == []


def test_labs_keep_observation_reference_missing_reference_and_opaque_document_id(tmp_path, monkeypatch):
    path = database(tmp_path)
    insert_contract_rows(path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    verified = dispatch_api(path, f"/api/v1/day/{STALE}", "")["labs"]
    missing = dispatch_api(path, f"/api/v1/day/{CURRENT}", "")["labs"]

    assert verified[0]["reference_status"] == "verified_observation_specific"
    assert verified[0]["reference"] == {"min": "0", "max": "5", "source": "scanned_original"}
    assert missing[0]["value"] == 0
    assert missing[0]["reference_status"] == "missing_reference"
    assert "reference" not in missing[0]
    assert missing[0]["document"]["id"].startswith("api-document-")
    serialized = json.dumps(missing)
    assert "/private/" not in serialized and "secret.invalid" not in serialized and "drive-secret" not in serialized


def test_multi_day_event_and_supported_future_appointment(tmp_path, monkeypatch):
    path = database(tmp_path)
    insert_contract_rows(path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    day = dispatch_api(path, f"/api/v1/day/{CURRENT}", "")
    appointment = dispatch_api(path, "/api/v1/day/2026-06-20", "")["appointments"]

    assert any(item["label"] == "SYNTHETIC_MULTI_DAY_PHASE" for item in day["events"]["periods"])
    assert appointment["status"] == "supported"
    assert appointment["items"][0]["status"] == "documented_visit"


def test_invalid_day_leap_day_future_bound_and_unknown_query(tmp_path, monkeypatch):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: date(2028, 2, 29))
    assert dispatch_api(path, "/api/v1/day/2028-02-29", "")["date"] == "2028-02-29"
    with pytest.raises(APIError) as invalid:
        dispatch_api(path, "/api/v1/day/2028-02-30", "")
    assert invalid.value.code == "invalid_date"
    with pytest.raises(APIError) as query:
        dispatch_api(path, "/api/v1/day/2028-02-29", "extra=1")
    assert query.value.code == "unknown_parameter"
    with pytest.raises(APIError) as future:
        dispatch_api(path, "/api/v1/day/2030-01-01", "")
    assert future.value.code == "future_date_not_allowed"


def test_zurich_today_contract_is_stable_across_dst_boundaries(tmp_path, monkeypatch):
    path = database(tmp_path)
    for boundary in (date(2026, 3, 29), date(2026, 10, 25)):
        monkeypatch.setattr(read_api, "local_today", lambda boundary=boundary: boundary)
        payload = dispatch_api(path, f"/api/v1/day/{boundary.isoformat()}", "")
        assert payload["today"] == boundary.isoformat()
        assert payload["timezone"] == "Europe/Zurich"
        assert payload["is_today"] is True


def test_calendar_accepts_62_days_rejects_63_and_reports_compact_days(tmp_path, monkeypatch):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    allowed = dispatch_api(path, "/api/v1/calendar", urlencode({"from": "2026-05-01", "to": "2026-07-01"}))
    assert len(allowed["days"]) == 62
    assert all(set(item) == {"date", "is_today", "categories", "counts", "completeness", "codes"} for item in allowed["days"])
    assert len(json.dumps(allowed).encode()) < 512_000
    with pytest.raises(APIError) as too_large:
        dispatch_api(path, "/api/v1/calendar", urlencode({"from": "2026-05-01", "to": "2026-07-02"}))
    assert too_large.value.code == "calendar_range_too_large"


def test_calendar_validates_dates_future_horizon_and_query_shape(tmp_path, monkeypatch):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    for query in (
        "from=2026-06-01&to=not-a-date",
        "from=2026-06-01&to=2026-06-30&table=laborwerte",
        "from=2026-06-01&from=2026-06-02&to=2026-06-30",
    ):
        with pytest.raises(APIError):
            dispatch_api(path, "/api/v1/calendar", query)
    with pytest.raises(APIError) as future:
        dispatch_api(path, "/api/v1/calendar", "from=2027-06-16&to=2027-06-17")
    assert future.value.code == "future_range_too_large"


def test_calendar_flag_is_default_off_and_auto_enables_explorer(tmp_path):
    path = database(tmp_path)
    connection = sqlite3.connect(path)
    connection.row_factory = sqlite3.Row
    try:
        from dashboard_v5.data_provider import build_bundle
        bundle = build_bundle(connection, today=TODAY.isoformat(), generated_at="2026-06-15T12:00:00+00:00")
    finally:
        connection.close()
    standard = render(bundle)
    enabled = render(bundle, calendar_day_6d=True)

    assert "data-calendar-day-enabled" not in standard
    assert "dashboard-v5-day-controller.js" not in standard
    assert "dashboard-v5-api-explorer.js" not in standard
    assert "data-calendar-day-enabled='1'" in enabled
    assert "dashboard-v5-day-controller.js" in enabled
    assert "dashboard-v5-api-explorer.js" in enabled
    assert "echarts-6.1.0.min.js" in enabled


def test_calendar_and_day_share_allowlisted_apple_zurich_day_assignment(tmp_path, monkeypatch):
    path = database(tmp_path)
    # 22:30 UTC on 14 June is 00:30 on 15 June in Europe/Zurich (CEST).
    insert_apple_record(
        path, metric="step_count", start_date="2026-06-14T22:30:00+00:00",
        value=321.0, unit="count", key="zurich-midnight",
    )
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    calendar = dispatch_api(path, "/api/v1/calendar", "from=2026-06-15&to=2026-06-15")
    day = dispatch_api(path, "/api/v1/day/2026-06-15", "")

    assert calendar["days"][0]["counts"]["measurements"] >= 1
    assert "measurements" in calendar["days"][0]["categories"]
    assert any(item["metric"] == "apple.steps" and item["value"] == 321 for item in day["measurements"])


def test_calendar_uses_canonical_zurich_day_at_dst_boundary(tmp_path, monkeypatch):
    path = database(tmp_path)
    # DST has started: 22:30 UTC on 29 March is 00:30 CEST on 30 March.
    insert_apple_record(
        path, metric="step_count", start_date="2026-03-29T22:30:00+00:00",
        value=654.0, unit="count", key="dst-boundary",
    )
    monkeypatch.setattr(read_api, "local_today", lambda: date(2026, 3, 31))
    calendar = dispatch_api(path, "/api/v1/calendar", "from=2026-03-29&to=2026-03-30")
    day = dispatch_api(path, "/api/v1/day/2026-03-30", "")

    assert calendar["days"][0]["counts"]["measurements"] == 0
    assert calendar["days"][1]["counts"]["measurements"] >= 1
    assert any(item["metric"] == "apple.steps" and item["value"] == 654 for item in day["measurements"])


def test_unreleased_apple_metric_is_absent_from_calendar_and_day(tmp_path, monkeypatch):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    baseline_calendar = dispatch_api(path, "/api/v1/calendar", "from=2026-06-12&to=2026-06-12")
    baseline_day = dispatch_api(path, "/api/v1/day/2026-06-12", "")
    insert_apple_record(
        path, metric="unreleased_synthetic_metric", start_date="2026-06-12T12:00:00+00:00",
        value=999.0, unit="count", key="unreleased",
    )
    calendar = dispatch_api(path, "/api/v1/calendar", "from=2026-06-12&to=2026-06-12")
    day = dispatch_api(path, "/api/v1/day/2026-06-12", "")

    assert calendar["days"][0]["counts"]["measurements"] == baseline_calendar["days"][0]["counts"]["measurements"]
    assert day["measurements"] == baseline_day["measurements"]
    assert all(item["metric"] != "unreleased_synthetic_metric" for item in day["measurements"])


def test_appointment_without_explicit_source_status_is_documented_not_occurred(tmp_path, monkeypatch):
    path = database(tmp_path)
    connection = sqlite3.connect(path)
    try:
        connection.execute(
            "INSERT INTO arztbesuche(datum,arzt,klinik,grund,notizen) VALUES(?,?,?,?,?)",
            ("2026-06-10", "Synthetic Clinician", "Synthetic Clinic", "Synthetic reason", "synthetic note"),
        )
        connection.commit()
    finally:
        connection.close()
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    appointment = dispatch_api(path, "/api/v1/day/2026-06-10", "")["appointments"]["items"]

    assert appointment[0]["status"] == "documented_visit"
    assert appointment[0]["status"] != "occurred"


def test_sprint6d_source_contract_has_no_legacy_apple_table_reference():
    source = (ROOT / "scripts" / "health" / "dashboard_v5" / "read_api.py").read_text(encoding="utf-8")
    documentation = (ROOT / "docs" / "sprint6d-calendar-day-drilldown.md").read_text(encoding="utf-8")
    evidence = (ROOT / "docs" / "test-results" / "sprint6d-integration-gate-20260715.json").read_text(encoding="utf-8")

    legacy_table = "apple_health_" + "quantities"
    assert legacy_table not in source
    assert legacy_table not in documentation
    assert legacy_table not in evidence
    assert "apple_health_records" in source
    assert "APPLE_SOURCE_SPECS" in source


def test_apple_raw_candidate_window_excludes_old_history_before_canonical_day_filter(tmp_path, monkeypatch):
    path = database(tmp_path)
    for index in range(4):
        insert_apple_record(
            path, metric="physical_effort", start_date=f"2020-01-0{index + 1}T12:00:00+00:00",
            value=1.0 + index, unit="kcal/h/kg", key=f"old-physical-effort-{index}",
        )
    # The UTC timestamp lands on 15 June in Europe/Zurich and must survive the
    # raw candidate window before daily_points performs the authoritative filter.
    insert_apple_record(
        path, metric="physical_effort", start_date="2026-06-14T22:30:00+00:00",
        value=7.5, unit="kcal/h/kg", key="target-physical-effort",
    )
    monkeypatch.setattr(read_api, "MAX_RAW_APPLE_ROWS", 3)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)

    series = dispatch_api(
        path, "/api/v1/series",
        urlencode({"metric": "apple.physical_effort", "from": "2026-06-15", "to": "2026-06-15", "resolution": "day"}),
    )
    calendar = dispatch_api(path, "/api/v1/calendar", "from=2026-06-15&to=2026-06-15")
    day = dispatch_api(path, "/api/v1/day/2026-06-15", "")

    assert series["points"] == [{"date": "2026-06-15", "value": 7.5, "quality": "direct", "source_class": "app", "baseline": None}]
    assert calendar["days"][0]["counts"]["measurements"] >= 1
    assert any(item["metric"] == "apple.physical_effort" and item["value"] == 7.5 for item in day["measurements"])
