from __future__ import annotations

from datetime import date, timedelta
import json
import os
from pathlib import Path
import sqlite3

import pytest

from tests.fixtures.dashboard_v5_fixture import build_dashboard_v5_fixture
from dashboard_v5.comparison_contract import _histamine_events
from dashboard_v5.observation_contract import (
    METHOD_VERSION,
    PUBLIC_CONTRACT_VERSION,
    TEMPLATES,
    validate_action,
)
import dashboard_v5.observation_engine as observation_engine
from dashboard_v5.observation_engine import evaluate_plan
from dashboard_v5.read_api import dispatch_api
from dashboard_v5.sprint6f_c_schema import apply_schema
import health_dashboard_action_worker as worker

os.environ.setdefault("HEALTH_DASHBOARD_FILE", "/tmp/health-7cc-test-v4.html")


def database(tmp_path: Path) -> Path:
    path = tmp_path / "synthetic.db"
    build_dashboard_v5_fixture(path, anchor_date=date(2026, 6, 15))
    connection = sqlite3.connect(path)
    apply_schema(connection)
    connection.commit()
    connection.close()
    return path


def action(title: str = "Sauna und folgende Nacht", status: str = "active") -> dict[str, object]:
    return validate_action(
        {
            "version": 1,
            "action": "observation_upsert",
            "observation_id": "",
            "title": title,
            "question": "Vorhandene Daten zeitlich gemeinsam betrachten.",
            "influences": ["event.sauna"],
            "outcomes": ["apple.sleep", "apple.hrv"],
            "lag_min": 0,
            "lag_max": 1,
            "start_date": "2026-06-01",
            "end_date": "2026-06-30",
            "status": status,
            "note": "",
            "include_doctor": False,
            "method_version": METHOD_VERSION,
        }
    )


def insert_plan(path: Path, influence: str = "event.sauna") -> str:
    identifier = "obs_aaaaaaaaaaaaaaaaaaaaaaaa"
    connection = sqlite3.connect(path)
    connection.execute(
        """INSERT INTO personal_observations
           (id,contract_version,title,question,influences_json,outcomes_json,lag_min,lag_max,
            start_date,end_date,status,note,include_doctor,method_version,created_at,updated_at)
           VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
        (
            identifier, 1, "Synthetische Beobachtung", "Nur dokumentierte Daten",
            json.dumps([influence]), json.dumps(["apple.sleep", "apple.hrv"]), 0, 1,
            "2026-06-01", "2026-06-20", "active", "", 0, METHOD_VERSION,
            "2026-06-01T00:00:00+02:00", "2026-06-01T00:00:00+02:00",
        ),
    )
    connection.commit()
    connection.close()
    return identifier


def event(identifier: str, day: str, influence: str = "event.sauna", review: str = "none", coverage: float | None = None) -> dict[str, object]:
    documented_entries = 10 if coverage is not None else None
    return {
        "id": identifier,
        "influence_id": influence,
        "date": day,
        "time": "21:00",
        "category": "Sauna" if influence == "event.sauna" else "Training",
        "title": "Dokumentiertes Ereignis",
        "details": [],
        "source": "Synthetische Quelle",
        "data_status": "documented",
        "review_status": review,
        "mapping_coverage": coverage,
        "mapped_entries": round(coverage * 10) if coverage is not None else None,
        "documented_entries": documented_entries,
    }


def loader(metric: str, start: date, end: date) -> dict[str, object]:
    values = []
    for index in range((end - start).days + 1):
        day = start + timedelta(days=index)
        values.append({"date": day.isoformat(), "value": (6.0 if metric == "apple.sleep" else 40.0) + index / 10})
    return {"label": metric, "unit": "h" if metric == "apple.sleep" else "ms", "source": {"type": "synthetic"}, "points": values}


def test_contract_reuses_v1_with_three_safe_templates_and_hard_limits() -> None:
    assert PUBLIC_CONTRACT_VERSION == "health.observation_plan.v1"
    assert [item[0] for item in TEMPLATES] == ["sauna_following_night", "training_recovery", "nutrition_symptoms"]
    with pytest.raises(ValueError, match="invalid_identifier_list"):
        validate_action({**action(), "outcomes": ["apple.sleep", "apple.hrv", "apple.resting_heart_rate"]})
    with pytest.raises(ValueError, match="invalid_identifier_list"):
        validate_action({**action(), "influences": ["event.sauna", "event.training"]})


def test_worker_allows_three_active_plans_and_rejects_a_fourth(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    path = database(tmp_path)
    monkeypatch.setattr(worker, "DASHBOARD_DB", path)
    for index in range(3):
        worker.apply_observation_action(action(f"Plan {index + 1}"))
    with pytest.raises(RuntimeError, match="maximum active"):
        worker.apply_observation_action(validate_action(action("Plan 4")))
    connection = sqlite3.connect(path)
    assert connection.execute("SELECT count(*) FROM personal_observations WHERE status='active'").fetchone()[0] == 3
    assert connection.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
    assert connection.execute("PRAGMA foreign_key_check").fetchall() == []
    connection.close()


def test_create_status_and_result_replay_are_idempotent(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    path = database(tmp_path)
    monkeypatch.setattr(worker, "DASHBOARD_DB", path)
    create = validate_action(action("Wiederholbarer Plan"))
    worker.apply_observation_action(create, "1" * 32)
    worker.apply_observation_action(create, "1" * 32)
    observation_id = "obs_" + "1" * 24
    complete = validate_action({"version": 1, "action": "observation_status", "observation_id": observation_id, "status": "completed"})
    worker.apply_observation_action(complete, "2" * 32)
    worker.apply_observation_action(complete, "2" * 32)
    recalculate = validate_action({"version": 1, "action": "observation_result_snapshot", "observation_id": observation_id})
    worker.apply_observation_action(recalculate, "3" * 32)
    worker.apply_observation_action(recalculate, "3" * 32)
    with sqlite3.connect(path) as connection:
        assert connection.execute("SELECT count(*) FROM personal_observations WHERE id=?", (observation_id,)).fetchone()[0] == 1
        assert connection.execute("SELECT count(*) FROM personal_observation_results WHERE observation_id=?", (observation_id,)).fetchone()[0] == 2


def test_following_sleep_day_thresholds_median_difference_and_companions(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    path = database(tmp_path)
    identifier = insert_plan(path)
    sauna = [event(f"sauna-{index}", f"2026-06-{index * 2 + 1:02d}") for index in range(5)]
    training = [event("training-context", "2026-06-02", "event.training")]

    def influences(_connection: sqlite3.Connection, influence_id: str, _start: date, _end: date):
        if influence_id == "event.sauna":
            return sauna, []
        if influence_id == "event.training":
            return training, []
        return [], []

    monkeypatch.setattr(observation_engine, "comparison_influence", influences)
    connection = sqlite3.connect(path)
    connection.row_factory = sqlite3.Row
    result = evaluate_plan(connection, identifier, loader, today=date(2026, 6, 15))
    connection.close()
    assert result["contract_version"] == PUBLIC_CONTRACT_VERSION
    assert result["event_count"] == 5
    assert result["events_with_followup"] == 5
    assert result["coverage"] == 1.0
    assert result["evaluable"] is True
    assert result["user_status"] == "evaluable"
    assert result["summaries"][0]["follow_median"] is not None
    assert result["summaries"][0]["numeric_difference"] is not None
    assert result["observations"][0]["outcomes"][0]["date"] == "2026-06-02"
    assert result["observations"][0]["companion_events"][0]["title"] == "Dokumentiertes Ereignis"
    assert "tatsächlich folgenden" in result["expected_window"]["sleep_rule"]
    assert "medizinisch bestätigte Wirkung" in result["medical_statement"]


def test_duplicate_events_cannot_reuse_one_followup_to_pass_the_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    path = database(tmp_path)
    identifier = insert_plan(path)
    events = [event(f"sauna-{index}", "2026-06-01") for index in range(5)]
    monkeypatch.setattr(observation_engine, "comparison_influence", lambda _c, influence_id, _s, _e: (events, []) if influence_id == "event.sauna" else ([], []))
    connection = sqlite3.connect(path); connection.row_factory = sqlite3.Row
    result = evaluate_plan(connection, identifier, loader, today=date(2026, 6, 15)); connection.close()
    assert result["event_count"] == 5
    assert result["events_with_followup"] == 1
    assert result["result_value_count"] == 2
    assert result["coverage"] == 0.2
    assert result["evaluable"] is False
    assert sum(outcome["duplicate_assignment"] for item in result["observations"] for outcome in item["outcomes"]) == 8


def test_event_on_plan_end_loads_the_following_day_without_extending_period_median(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    path = database(tmp_path)
    identifier = insert_plan(path)
    final_event = [event("final-sauna", "2026-06-20")]
    monkeypatch.setattr(observation_engine, "comparison_influence", lambda _c, influence_id, _s, _e: (final_event, []) if influence_id == "event.sauna" else ([], []))
    connection = sqlite3.connect(path); connection.row_factory = sqlite3.Row
    result = evaluate_plan(connection, identifier, loader, today=date(2026, 6, 25)); connection.close()
    assert result["observations"][0]["outcomes"][0]["date"] == "2026-06-21"
    assert result["comparison"]["to"] == "2026-06-21"
    assert result["summaries"][0]["period_value_count"] == 20


def test_missing_followups_keep_individual_events_but_hide_summary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    path = database(tmp_path)
    identifier = insert_plan(path)
    sauna = [event(f"sauna-{index}", f"2026-06-{index + 1:02d}") for index in range(5)]
    monkeypatch.setattr(observation_engine, "comparison_influence", lambda _c, influence_id, _s, _e: (sauna, []) if influence_id == "event.sauna" else ([], []))

    def sparse(metric: str, _start: date, _end: date) -> dict[str, object]:
        return {"label": metric, "unit": "u", "source": {"type": "synthetic"}, "points": [{"date": "2026-06-02", "value": 1}, {"date": "2026-06-03", "value": 2}]}

    connection = sqlite3.connect(path); connection.row_factory = sqlite3.Row
    result = evaluate_plan(connection, identifier, sparse, today=date(2026, 6, 15)); connection.close()
    assert len(result["observations"]) == 5
    assert result["evaluable"] is False
    assert result["summary_label"] == "Noch nicht genügend Beobachtungen für eine Zusammenfassung."
    assert all("follow_median" not in item for item in result["summaries"])


def test_open_histamine_mapping_blocks_group_summary_and_never_creates_histamine_free_group(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    path = database(tmp_path)
    identifier = insert_plan(path, "nutrition.histamine")
    histamine = [event(f"food-{index}", f"2026-06-{index + 1:02d}", "nutrition.histamine", "complete_mapping", 1.0) for index in range(4)]
    histamine.append(event("food-4", "2026-06-05", "nutrition.histamine", "review_open", 0.5))
    monkeypatch.setattr(observation_engine, "comparison_influence", lambda _c, influence_id, _s, _e: (histamine, []) if influence_id == "nutrition.histamine" else ([], []))
    connection = sqlite3.connect(path); connection.row_factory = sqlite3.Row
    result = evaluate_plan(connection, identifier, loader, today=date(2026, 6, 15)); connection.close()
    assert result["histamine"]["review_open"] is True
    assert result["histamine"]["mapping_coverage"] == 0.9
    assert result["evaluable"] is False
    assert "histaminfrei" in result["histamine"]["statement"]


def test_completely_unmapped_nutrition_day_remains_visible_as_open_mapping(tmp_path: Path) -> None:
    path = database(tmp_path)
    with sqlite3.connect(path) as connection:
        connection.row_factory = sqlite3.Row
        connection.execute("DELETE FROM nutrition_daily_summary_v2")
        connection.execute(
            "INSERT INTO nutrition_daily_summary_v2(datum,item_count,histamine_unknown_count,histamine_max) VALUES('2026-06-12',3,3,NULL)"
        )
        events = _histamine_events(connection, date(2026, 6, 12), date(2026, 6, 12))
    assert len(events) == 1
    assert events[0]["mapped_entries"] == 0
    assert events[0]["review_status"] == "review_open"
    assert events[0]["data_status"] == "unmapped"
    assert "Zuordnung offen" in events[0]["title"]


def test_read_api_today_is_silent_and_ui_hands_plan_to_existing_comparison(tmp_path: Path) -> None:
    path = database(tmp_path)
    templates = dispatch_api(path, "/api/v1/observations/templates", "")
    today = dispatch_api(path, "/api/v1/observations/active-today", "")
    assert templates["contract_version"] == PUBLIC_CONTRACT_VERSION
    assert len(templates["templates"]) == 3
    assert today == {"version": 1, "contract_version": PUBLIC_CONTRACT_VERSION, "task": None, "reason": "no_unambiguous_user_action"}
    source = Path("scripts/health/assets/health-assets/dashboard-v5-observations.js").read_text()
    assert "comparison_metrics" in source and "comparison_influences" in source
    assert "In der Vergleichsansicht öffnen" in source
    assert "Beobachtung ergänzen" not in source
    assert "window.healthDayRouter" in source
