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.association_engine import (  # noqa: E402
    calculation_status,
    pair_series,
    ranks,
    spearman,
)
from dashboard_v5.read_api import APIError, dispatch_api  # noqa: E402

_FIXTURE_PATH = ROOT / "tests" / "fixtures" / "dashboard_v5_fixture.py"
_loader = importlib.util.spec_from_file_location("association_fixture", _FIXTURE_PATH)
assert _loader and _loader.loader
_fixture = importlib.util.module_from_spec(_loader)
_loader.loader.exec_module(_fixture)
build_dashboard_v5_fixture = _fixture.build_dashboard_v5_fixture
TODAY = date(2026, 6, 15)


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


def query(question: str, influence: str, target: str, lag: int = 0) -> str:
    return urlencode(
        {
            "question": question,
            "influence": influence,
            "target": target,
            "from": "2026-05-20",
            "to": TODAY.isoformat(),
            "lag": str(lag),
        }
    )


def test_spearman_uses_average_tie_ranks_and_minimum_statuses():
    assert ranks([2, 1, 2]) == [2.5, 1.0, 2.5]
    assert spearman([1, 2, 3, 4, 5], [10, 20, 30, 40, 50]) == 1.0
    assert calculation_status(4, 1.0) == "not_calculable"
    assert calculation_status(5, 1.0) == "descriptive_only"
    assert calculation_status(10, 1.0) == "preliminary"
    assert calculation_status(20, 1.0) == "exploratory"


def test_lag_pairs_influence_day_to_later_target_without_filling_missing_days():
    influence = {"2026-03-28": 1.0, "2026-03-29": 2.0, "2026-03-30": 3.0}
    target = {"2026-03-29": 10.0, "2026-03-31": 30.0}
    assert pair_series(influence, target, 1) == [
        {"date": "2026-03-29", "influence_date": "2026-03-28", "x": 1.0, "y": 10.0},
        {"date": "2026-03-31", "influence_date": "2026-03-30", "x": 3.0, "y": 30.0},
    ]


def test_continuous_api_is_deterministic_and_exposes_quality_heatmap(
    tmp_path, monkeypatch
):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    request = query("continuous", "apple.sleep", "apple.hrv", 1)
    first = dispatch_api(path, "/api/v1/associations", request)
    second = dispatch_api(path, "/api/v1/associations", request)
    assert json.dumps(first, sort_keys=True) == json.dumps(second, sort_keys=True)
    assert first["method"] == "spearman_rank_complete_case"
    assert first["lag_days"] == 1
    assert first["quality"]["paired_days"] == len(first["pairs"])
    assert len(first["heatmap"]) == 8
    assert all(item["lag"] in range(8) for item in first["heatmap"])
    assert all("date" in pair and "influence_date" in pair for pair in first["pairs"])


def test_event_target_under_three_events_stays_single_case_without_correlation(
    tmp_path, monkeypatch
):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    connection = sqlite3.connect(path)
    connection.execute("DELETE FROM symptom_log WHERE symptom='Aphthen/Mundulzera'")
    connection.execute(
        "INSERT INTO symptom_log(datum,symptom,schwergrad,kontext) VALUES(?,?,?,?)",
        ("2026-06-10", "Aphthen/Mundulzera", "2", "additional_symptom"),
    )
    connection.commit()
    connection.close()
    data = dispatch_api(
        path,
        "/api/v1/associations",
        query("before_event", "apple.sleep", "event.aphthae", 2),
    )
    assert data["coefficient"] is None
    assert data["quality"]["status"] == "single_case_only"
    assert data["event_summary"]["event_count"] == 1
    assert data["event_timelines"][0]["event_date"] == "2026-06-10"


def test_labor_target_is_not_interpolated_after_medication(tmp_path, monkeypatch):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    data = dispatch_api(
        path,
        "/api/v1/associations",
        urlencode(
            {
                "question": "after_event",
                "influence": "event.medication",
                "target": "lab.crp",
                "from": "2025-05-20",
                "to": TODAY.isoformat(),
                "lag": "1",
            }
        ),
    )
    observed_lab_days = {item["date"] for item in data["series"]["target"]}
    assert observed_lab_days
    assert all(item["value"] is not None for item in data["series"]["target"])
    assert len(observed_lab_days) == len(data["series"]["target"])


@pytest.mark.parametrize(
    ("changes", "code"),
    [
        ({"influence": "raw.sql_field"}, "influence_not_allowed"),
        ({"lag": "15"}, "lag_out_of_range"),
        ({"question": "continuous", "target": "event.aphthae"}, "incompatible_metrics"),
    ],
)
def test_api_allowlist_lag_limit_and_compatibility_fail_closed(
    tmp_path, monkeypatch, changes, code
):
    path = database(tmp_path)
    monkeypatch.setattr(read_api, "local_today", lambda: TODAY)
    params = dict(
        question="continuous",
        influence="apple.sleep",
        target="apple.hrv",
        **{"from": "2026-05-20", "to": TODAY.isoformat(), "lag": "0"},
    )
    params.update(changes)
    with pytest.raises(APIError) as caught:
        dispatch_api(path, "/api/v1/associations", urlencode(params))
    assert caught.value.code == code


def test_catalog_exposes_only_fixed_labels_and_no_internal_identifiers(tmp_path):
    data = dispatch_api(database(tmp_path), "/api/v1/associations/catalog", "")
    assert data["method_version"] == "health_temporal_association_v1"
    assert {item["id"] for item in data["questions"]} == {
        "continuous",
        "before_event",
        "after_event",
    }
    serialized = json.dumps(data)
    assert (
        "/home/" not in serialized
        and "SELECT " not in serialized
        and "drive" not in serialized.casefold()
    )
    assert all(0 <= lag <= 14 for lag in data["lags"])
