from __future__ import annotations

import importlib.util
import sqlite3
import sys
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo

import pytest

ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = ROOT / "scripts/health/apple_health_analytics.py"


def load_module(name: str = "apple_health_analytics_v2_test"):
    spec = importlib.util.spec_from_file_location(name, MODULE_PATH)
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    spec.loader.exec_module(module)
    return module


def make_db(path: Path) -> None:
    con = sqlite3.connect(path)
    con.executescript(
        """
        CREATE TABLE apple_health_records (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            metric TEXT NOT NULL,
            start_date TEXT,
            end_date TEXT,
            value REAL,
            unit TEXT,
            source_name TEXT,
            device TEXT,
            file_name TEXT,
            file_hash TEXT,
            imported_at TEXT DEFAULT CURRENT_TIMESTAMP
        );
        """
    )
    con.close()


def insert_record(
    db: Path,
    *,
    metric: str,
    start: str,
    value: float,
    unit: str = "count",
    source: str = "Apple Watch Test",
    file_name: str = "HealthAutoExport_jahr-2026-01-02.json",
    file_hash: str = "daily-a",
    end: str | None = None,
) -> None:
    con = sqlite3.connect(db)
    con.execute(
        """INSERT INTO apple_health_records
           (metric,start_date,end_date,value,unit,source_name,device,file_name,file_hash)
           VALUES(?,?,?,?,?,?,?,?,?)""",
        (metric, start, end or start, value, unit, source, "Synthetic Device", file_name, file_hash),
    )
    con.commit()
    con.close()


@pytest.fixture
def analytics(tmp_path, monkeypatch):
    db = tmp_path / "health.db"
    make_db(db)
    mod = load_module()
    monkeypatch.setattr(mod, "DB", db)
    return mod, db


def test_exact_observation_from_overlapping_exports_is_deduplicated(analytics):
    mod, db = analytics
    for file_hash in ("export-old", "export-new"):
        insert_record(db, metric="step_count", start="2026-01-02T12:00:00", value=100, file_hash=file_hash)

    points = mod.daily_points("step_count", now=datetime(2026, 1, 5, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 100
    assert points["2026-01-02"].deduplicated_records == 1


def test_fine_daily_export_supersedes_weekly_fallback_without_double_counting(analytics):
    mod, db = analytics
    for day, value in (("2026-01-01", 700), ("2026-01-08", 1400), ("2026-01-15", 2100)):
        insert_record(
            db,
            metric="step_count",
            start=f"{day}T00:00:00",
            value=value,
            file_name="HealthAutoExport_jahr-2026.json",
            file_hash="annual-weekly",
        )
    insert_record(
        db,
        metric="step_count",
        start="2026-01-08T18:00:00",
        value=250,
        file_name="HealthAutoExport_jahr-2026-01-09.json",
        file_hash="daily-direct",
    )

    points = mod.daily_points("step_count", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-01"].value == 100
    assert points["2026-01-01"].quality == "coarse_fallback"
    assert points["2026-01-08"].value == 250
    assert points["2026-01-08"].quality == "direct"
    assert points["2026-01-08"].discarded_overlap_records == 1


def test_direct_resolution_wins_even_if_coarse_duplicate_was_imported_later(analytics):
    mod, db = analytics
    insert_record(
        db,
        metric="step_count",
        start="2026-01-08T00:00:00",
        value=250,
        file_name="HealthAutoExport_jahr-2026-01-09.json",
        file_hash="daily-direct",
    )
    for day, value in (("2026-01-01", 700), ("2026-01-08", 1400), ("2026-01-15", 2100)):
        insert_record(
            db,
            metric="step_count",
            start=f"{day}T00:00:00",
            value=value,
            file_name="HealthAutoExport_jahr-2026.json",
            file_hash="annual-weekly",
        )

    points = mod.daily_points("step_count", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-08"].value == 250
    assert points["2026-01-08"].quality == "direct"
    assert points["2026-01-08"].deduplicated_records == 1
    assert points["2026-01-08"].discarded_overlap_records == 0


def test_regular_sparse_direct_file_is_not_misclassified_as_coarse(analytics):
    mod, db = analytics
    for day, value in (("2026-01-01", 100), ("2026-01-03", 200), ("2026-01-05", 300)):
        insert_record(
            db,
            metric="step_count",
            start=f"{day}T12:00:00",
            value=value,
            file_name="HealthAutoExport_2026-01-06.json",
            file_hash="ordinary-direct-export",
        )

    points = mod.daily_points("step_count", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert [points[day].value for day in sorted(points)] == [100, 200, 300]
    assert all(point.quality == "direct" for point in points.values())


def test_equivalent_offsets_and_energy_units_deduplicate_after_normalization(analytics):
    mod, db = analytics
    insert_record(
        db,
        metric="active_energy",
        start="2026-01-02T12:00:00+01:00",
        value=100,
        unit="kcal",
        source="Apple Watch Alpha",
        file_hash="export-a",
    )
    insert_record(
        db,
        metric="active_energy",
        start="2026-01-02T11:00:00Z",
        value=418.4,
        unit="kJ",
        source="Apple Watch Beta",
        file_hash="export-b",
    )

    points = mod.daily_points("active_energy", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 100
    assert points["2026-01-02"].deduplicated_records == 1


def test_incomplete_annual_grouped_export_is_still_coarse(analytics):
    mod, db = analytics
    insert_record(
        db,
        metric="step_count",
        start="2026-01-01T00:00:00",
        value=700,
        file_name="HealthAutoExport_jahr-2026.json",
        file_hash="incomplete-annual",
    )

    points = mod.daily_points("step_count", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-01"].value == 100
    assert points["2026-01-01"].quality == "coarse_fallback"


def test_unit_for_noncanonical_metric_uses_injected_rows_without_global_db(tmp_path):
    module = load_module("apple_health_analytics_unit_injection_test")
    db = tmp_path / "rows.db"
    make_db(db)
    insert_record(
        db,
        metric="physical_effort",
        start="2025-01-02T12:00:00+01:00",
        value=4.2,
        unit="MET",
    )
    connection = sqlite3.connect(db)
    connection.row_factory = sqlite3.Row
    rows = list(connection.execute("SELECT * FROM apple_health_records"))
    connection.close()
    setattr(module, "DB", tmp_path / "must-not-exist" / "health.db")

    assert module.unit_for("physical_effort", _rows=rows) == "MET"


@pytest.mark.parametrize(
    ("file_name", "value", "expected"),
    (("HealthAutoExport_weekly.json", 700, 100), ("HealthAutoExport_monthly.json", 900, 30)),
)
def test_incomplete_explicit_weekly_and_monthly_exports_are_coarse(analytics, file_name, value, expected):
    mod, db = analytics
    insert_record(
        db,
        metric="step_count",
        start="2026-01-01T00:00:00",
        value=value,
        file_name=file_name,
        file_hash=file_name,
    )

    points = mod.daily_points("step_count", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-01"].value == expected
    assert points["2026-01-01"].quality == "coarse_fallback"


def test_same_source_class_aliases_are_combined_without_discarding_distinct_samples(analytics):
    mod, db = analytics
    insert_record(db, metric="step_count", start="2026-01-02T10:00:00", value=100, source="Apple Watch Alpha")
    insert_record(db, metric="step_count", start="2026-01-02T11:00:00", value=200, source="Apple Watch Beta", file_hash="daily-b")

    points = mod.daily_points("step_count", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 300
    assert points["2026-01-02"].source_class == "watch"
    assert points["2026-01-02"].selected_records == 2
    assert points["2026-01-02"].discarded_source_records == 0


def test_same_timestamp_but_different_values_are_not_deduplicated(analytics):
    mod, db = analytics
    insert_record(db, metric="step_count", start="2026-01-02T10:00:00", value=100, source="Apple Watch Alpha")
    insert_record(
        db,
        metric="step_count",
        start="2026-01-02T10:00:00",
        value=200,
        source="Apple Watch Beta",
        file_hash="daily-b",
    )

    points = mod.daily_points("step_count", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 300
    assert points["2026-01-02"].selected_records == 2
    assert points["2026-01-02"].deduplicated_records == 0


def test_same_exact_source_changed_value_is_treated_as_newer_export_revision(analytics):
    mod, db = analytics
    insert_record(db, metric="step_count", start="2026-01-02T10:00:00", value=100, source="Apple Watch Alpha")
    insert_record(
        db,
        metric="step_count",
        start="2026-01-02T10:00:00",
        value=200,
        source="Apple Watch Alpha",
        file_hash="newer-export",
    )

    points = mod.daily_points("step_count", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 200
    assert points["2026-01-02"].selected_records == 1
    assert points["2026-01-02"].deduplicated_records == 1


def test_compatible_distance_units_are_normalized_before_daily_sum(analytics):
    mod, db = analytics
    insert_record(db, metric="walking_running_distance", start="2026-01-02T10:00:00", value=1000, unit="m")
    insert_record(db, metric="walking_running_distance", start="2026-01-02T11:00:00", value=1, unit="km", file_hash="daily-b")

    points = mod.daily_points("walking_running_distance", now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 2
    assert points["2026-01-02"].unit == "km"
    assert mod.unit_for("walking_running_distance") == "km / Tag*"


def test_supported_compatible_units_normalize_to_canonical_units(analytics):
    mod, _db = analytics
    cases = (
        ("weight_body_mass", 70000, "g", 70, "kg"),
        ("dietary_water", 1.5, "L", 1500, "mL"),
        ("sleep_analysis", 480, "min", 8, "hr"),
        ("apple_exercise_time", 1, "hr", 60, "min"),
    )
    for metric, value, unit, expected_value, expected_unit in cases:
        normalized_value, normalized_unit = mod._normalize_value(metric, value, unit)
        assert normalized_value == pytest.approx(expected_value)
        assert normalized_unit == expected_unit


def test_summary_reuses_single_metadata_load_for_all_metrics(analytics, monkeypatch):
    mod, db = analytics
    insert_record(db, metric="heart_rate", start="2026-01-02T10:00:00", value=60, unit="count/min")
    insert_record(db, metric="step_count", start="2026-01-02T10:00:00", value=100, unit="count", file_hash="daily-b")
    original = mod._load_rows
    calls = []

    def tracked(*args, **kwargs):
        calls.append((args, kwargs))
        return original(*args, **kwargs)

    monkeypatch.setattr(mod, "_load_rows", tracked)
    result = mod.summary()

    assert result["metric_count"] == 2
    assert len(calls) == 1


def test_source_precedence_avoids_summing_watch_and_phone_for_same_day(analytics):
    mod, db = analytics
    insert_record(db, metric="step_count", start="2026-01-02T12:00:00", value=100, source="Apple Watch Test")
    insert_record(db, metric="step_count", start="2026-01-02T12:30:00", value=80, source="iPhone Test", file_hash="daily-b")

    points = mod.daily_points("step_count", now=datetime(2026, 1, 5, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 100
    assert points["2026-01-02"].source_class == "watch"
    assert points["2026-01-02"].discarded_source_records == 1


def test_timezone_aware_records_are_bucketed_in_zurich_local_day(analytics):
    mod, db = analytics
    insert_record(db, metric="heart_rate", start="2026-01-01T23:30:00+00:00", value=60, unit="count/min")

    series = mod.daily_series("heart_rate", now=datetime(2026, 1, 5, tzinfo=ZoneInfo("Europe/Zurich")))

    assert series == {"2026-01-02": 60}


def test_naive_records_are_explicitly_interpreted_as_zurich_local_time(analytics):
    mod, db = analytics
    insert_record(db, metric="heart_rate", start="2026-01-02T00:30:00", value=60, unit="count/min")

    points = mod.daily_points("heart_rate", now=datetime(2026, 1, 5, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].timezone_assumption == "naive_assumed_europe_zurich"


def test_current_local_day_is_excluded_as_provisional_by_default(analytics):
    mod, db = analytics
    insert_record(db, metric="step_count", start="2026-01-02T12:00:00", value=100)

    assert mod.daily_series("step_count", now=datetime(2026, 1, 2, 18, tzinfo=ZoneInfo("Europe/Zurich"))) == {}
    assert mod.daily_series("step_count", stable_only=False, now=datetime(2026, 1, 2, 18, tzinfo=ZoneInfo("Europe/Zurich"))) == {"2026-01-02": 100}


def test_energy_units_are_normalized_to_kcal(analytics):
    mod, db = analytics
    insert_record(db, metric="active_energy", start="2026-01-02T12:00:00", value=418.4, unit="kJ")

    points = mod.daily_points("active_energy", now=datetime(2026, 1, 5, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 100
    assert points["2026-01-02"].unit == "kcal"


def test_last_value_semantics_for_body_measurements(analytics):
    mod, db = analytics
    insert_record(db, metric="weight_body_mass", start="2026-01-02T08:00:00", value=70, unit="kg")
    insert_record(db, metric="weight_body_mass", start="2026-01-02T18:00:00", value=71, unit="kg", file_hash="daily-b")

    points = mod.daily_points("weight_body_mass", now=datetime(2026, 1, 5, tzinfo=ZoneInfo("Europe/Zurich")))

    assert points["2026-01-02"].value == 71


def test_coverage_summary_reports_missing_and_fallback_days_without_raw_values(analytics):
    mod, db = analytics
    insert_record(db, metric="heart_rate", start="2026-01-01T12:00:00", value=60, unit="count/min")
    insert_record(db, metric="heart_rate", start="2026-01-03T12:00:00", value=62, unit="count/min", file_hash="daily-b")
    insert_record(db, metric="heart_rate", start="2025-12-01T12:00:00", value=58, unit="count/min", file_hash="old-a")
    insert_record(db, metric="heart_rate", start="2025-12-01T12:00:00", value=58, unit="count/min", file_hash="old-b")

    quality = mod.coverage_summary(
        "heart_rate",
        start_date="2026-01-01",
        end_date="2026-01-03",
        now=datetime(2026, 1, 5, tzinfo=ZoneInfo("Europe/Zurich")),
    )

    assert quality["expected_days"] == 3
    assert quality["observed_days"] == 2
    assert quality["missing_days"] == 1
    assert quality["coverage_ratio"] == pytest.approx(2 / 3, abs=0.001)
    assert quality["deduplicated_records"] == 0
    assert "values" not in quality
    assert "source_names" not in quality


def test_body_measurement_coverage_uses_weekly_expected_cadence(analytics):
    mod, db = analytics
    insert_record(db, metric="weight_body_mass", start="2026-01-10T08:00:00", value=70, unit="kg")

    quality = mod.coverage_summary(
        "weight_body_mass",
        start_date="2026-01-01",
        end_date="2026-01-30",
        now=datetime(2026, 2, 1, tzinfo=ZoneInfo("Europe/Zurich")),
    )

    assert quality["expected_cadence_days"] == 7
    assert quality["expected_days"] == 30
    assert quality["expected_observations"] == 5
    assert quality["observed_days"] == 1
    assert quality["coverage_ratio"] == 0.2


def test_dashboard_exposes_data_quality_as_non_medical_metadata_only():
    source = (ROOT / "scripts/health/health_dashboard_v3.py").read_text(encoding="utf-8")
    assert "apple_coverage_summary" in source
    assert "datetime.now(APPLE_LOCAL_TZ).date()" in source
    assert "Datenqualität der letzten 30 abgeschlossenen Tage" in source
    assert "bewertet ausschließlich Datenabdeckung und Importqualität, nicht Gesundheit oder medizinisches Risiko" in source


def test_analytics_queries_exclude_raw_json_payloads():
    source = MODULE_PATH.read_text(encoding="utf-8")
    assert "SELECT * FROM apple_health_records" not in source
    assert "SELECT id,metric,value,unit,start_date,end_date,source_name,file_name,file_hash" in source
