from __future__ import annotations

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

import pytest

from tests.fixtures.dashboard_v5_fixture import build_dashboard_v5_fixture
from dashboard_v5.observation_contract import (
    ALLOWED_IDS,
    TEMPLATES,
    validate_action as validate_observation_payload,
)
from dashboard_v5.observation_engine import evaluate_observation
from dashboard_v5.read_api import dispatch_api
from dashboard_v5.sprint6f_c_schema import apply_schema, assert_schema
from dashboard_v5.sprint6f_c_schema import METHOD_VERSION
from dashboard_v5.data_provider import build_bundle
from dashboard_v5.render import render
import health_dashboard_action_worker as worker
os.environ.setdefault("HEALTH_DASHBOARD_FILE", "/tmp/health-6fc-test-v4.html")
from health_dashboard_server import validate_observation_submission  # noqa: E402


def payload(action: str, **values: object) -> dict[str, object]:
    return validate_observation_payload({"version": 1, "action": action, **values})


def question(**changes: object) -> dict[str, object]:
    values: dict[str, object] = {
        "observation_id": "",
        "title": "Histamin und Aphten",
        "question": "Folgen auf dokumentierte Ernährungstage häufiger Aphten?",
        "influences": ["nutrition.histamine"],
        "outcomes": ["event.aphthae"],
        "lag_min": 0,
        "lag_max": 3,
        "start_date": "2026-05-01",
        "end_date": "2026-06-15",
        "status": "active",
        "note": "Synthetische Testnotiz",
        "include_doctor": True,
        "method_version": METHOD_VERSION,
    }
    values.update(changes)
    return payload("observation_upsert", **values)


def phase(observation_id: str, **changes: object) -> dict[str, object]:
    values: dict[str, object] = {
        "observation_id": observation_id,
        "phase_id": "",
        "phase_type": "baseline",
        "name": "Ausgangsphase",
        "start_date": "2026-05-01",
        "end_date": "2026-05-14",
        "behavior_goal": "Nur bereits gewählte Beobachtung dokumentieren",
        "metrics": ["nutrition.histamine"],
        "events": ["event.aphthae"],
        "lag_min": 0,
        "lag_max": 3,
        "adherence": "unknown",
        "notes": "",
    }
    values.update(changes)
    return payload("observation_phase_upsert", **values)


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


def test_schema_is_additive_idempotent_and_templates_are_allowlisted(tmp_path: Path) -> None:
    database = synthetic_database(tmp_path)
    connection = sqlite3.connect(database)
    before = connection.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table'").fetchone()[0]
    apply_schema(connection)
    apply_schema(connection)
    assert_schema(connection)
    after = connection.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table'").fetchone()[0]
    assert before == after
    assert connection.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
    assert connection.execute("PRAGMA foreign_key_check").fetchall() == []
    connection.close()
    assert len(TEMPLATES) == 3
    assert all(set(item[2] + item[3]) <= ALLOWED_IDS for item in TEMPLATES)


def test_contract_fails_closed_for_unknown_metrics_ranges_and_transitions() -> None:
    with pytest.raises(ValueError, match="identifier_not_allowed"):
        question(influences=["invented.metric"])
    with pytest.raises(ValueError, match="invalid_lag"):
        question(lag_min=8, lag_max=7)
    with pytest.raises(ValueError, match="invalid_date_range"):
        question(start_date="2026-06-02", end_date="2026-06-01")
    with pytest.raises(ValueError, match="invalid_opaque_id"):
        payload("observation_status", observation_id="17", status="completed")


def test_server_form_contract_keeps_technical_ids_canonical() -> None:
    form = {
        "csrf_token": ["x"],
        "return_to": ["v5"],
        "mode": ["question"],
        "observation_id": [""],
        "title": ["Histamin und Aphten"],
        "question": ["Persönliche Beobachtung"],
        "influences": ["nutrition.histamine"],
        "outcomes": ["event.aphthae"],
        "lag_min": ["0"],
        "lag_max": ["3"],
        "start_date": ["2026-05-01"],
        "end_date": ["2026-06-15"],
        "status": ["draft"],
        "note": [""],
        "include_doctor": ["1"],
    }
    normalized, _ = validate_observation_submission(form)
    assert normalized["influences"] == ["nutrition.histamine"]
    broken = dict(form)
    broken["influences"] = ["nutrition.histamine,unknown.metric"]
    with pytest.raises(ValueError):
        validate_observation_submission(broken)


def test_worker_lifecycle_checkin_and_immutable_result_versions(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    database = synthetic_database(tmp_path)
    monkeypatch.setattr(worker, "DASHBOARD_DB", database)
    worker.apply_observation_action(question())
    connection = sqlite3.connect(database)
    observation_id = connection.execute("SELECT id FROM personal_observations").fetchone()[0]
    connection.close()
    worker.apply_observation_action(phase(observation_id))
    worker.apply_observation_action(
        phase(
            observation_id,
            phase_type="observation",
            name="Beobachtungsphase",
            start_date="2026-05-15",
            end_date="2026-06-15",
        )
    )
    checkin = payload(
        "observation_checkin",
        observation_id=observation_id,
        phase_id="",
        day="2026-05-05",
        adherence="partial",
        stress="unknown",
        sleep_disruption="unknown",
        infection="unknown",
        unusual_activity="unknown",
        travel="unknown",
        medication_change_fact="",
        supplement_change_fact="",
        note="Synthetischer Check-in",
    )
    worker.apply_observation_action(checkin)
    worker.apply_observation_action(
        payload("observation_status", observation_id=observation_id, status="completed")
    )
    connection = sqlite3.connect(database)
    first = connection.execute(
        "SELECT id,result_version,configuration_hash,summary_json FROM personal_observation_results"
    ).fetchone()
    assert first and first[1] == 1
    first_json = first[3]
    connection.close()
    worker.apply_observation_action(
        payload("observation_result_snapshot", observation_id=observation_id)
    )
    connection = sqlite3.connect(database)
    rows = connection.execute(
        "SELECT result_version,configuration_hash,summary_json FROM personal_observation_results ORDER BY result_version"
    ).fetchall()
    assert [row[0] for row in rows] == [1, 2]
    assert rows[0][2] == first_json
    assert rows[0][1] == rows[1][1]
    with pytest.raises(sqlite3.IntegrityError, match="immutable personal observation result"):
        connection.execute("UPDATE personal_observation_results SET summary_json='{}' WHERE result_version=1")
    assert connection.execute("SELECT COUNT(*) FROM personal_observation_checkins").fetchone()[0] == 1
    connection.close()


def test_phase_evaluation_thresholds_missing_days_and_baseline_are_descriptive(tmp_path: Path) -> None:
    database = synthetic_database(tmp_path)
    connection = sqlite3.connect(database)
    connection.row_factory = sqlite3.Row
    connection.execute(
        """INSERT INTO personal_observations VALUES
        ('obs_aaaaaaaaaaaaaaaaaaaaaaaa',1,'Synthetische Frage','Frage', '["apple.hrv"]','["apple.hrv"]',0,3,
        '2026-01-01','2026-01-30','active','',0,'personal-observation-phases-v1','2026-01-01T00:00:00','2026-01-01T00:00:00')"""
    )
    specs = [
        ("phase_aaaaaaaaaaaaaaaaaaaaaaaa", "baseline", "Kurz", "2026-01-01", "2026-01-06", 6),
        ("phase_bbbbbbbbbbbbbbbbbbbbbbbb", "observation", "Deskriptiv", "2026-01-07", "2026-01-16", 10),
        ("phase_cccccccccccccccccccccccc", "followup", "Vergleich", "2026-01-17", "2026-01-30", 14),
    ]
    for phase_id, phase_type, name, start, end, _ in specs:
        connection.execute(
            """INSERT INTO personal_observation_phases VALUES
            (?,?,?,?,?,?,'','["apple.hrv"]','[]',0,3,'unknown','','2026-01-01T00:00:00','2026-01-01T00:00:00')""",
            (phase_id, "obs_aaaaaaaaaaaaaaaaaaaaaaaa", phase_type, name, start, end),
        )
    connection.commit()

    def loader(_metric: str, start: date, end: date) -> dict[str, object]:
        count = (end - start).days + 1
        points = [
            {"date": (start + timedelta(days=index)).isoformat(), "value": 40 + index}
            for index in range(count)
        ]
        if count == 14:
            points.pop(4)
        return {"points": points, "unit": "ms", "source": {"type": "synthetic"}}

    result = evaluate_observation(connection, "obs_aaaaaaaaaaaaaaaaaaaaaaaa", loader)
    statuses = [item["metrics"]["apple.hrv"]["status"] for item in result["phases"]]
    assert statuses == ["insufficient", "descriptive", "descriptive"]
    assert "median" not in result["phases"][0]["metrics"]["apple.hrv"]
    assert result["phases"][2]["metrics"]["apple.hrv"]["missing_days"] == 1
    assert result["medical_statement"].startswith("Zeitliche persönliche Beobachtung")
    connection.close()


def test_read_api_lists_reopens_analysis_and_exact_result(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    database = synthetic_database(tmp_path)
    monkeypatch.setattr(worker, "DASHBOARD_DB", database)
    worker.apply_observation_action(question(status="active"))
    connection = sqlite3.connect(database)
    observation_id = connection.execute("SELECT id FROM personal_observations").fetchone()[0]
    connection.close()
    worker.apply_observation_action(payload("observation_status", observation_id=observation_id, status="completed"))
    connection = sqlite3.connect(database)
    result_id = connection.execute("SELECT id FROM personal_observation_results").fetchone()[0]
    connection.close()
    listing = dispatch_api(database, "/api/v1/observations", "status=completed")
    assert listing["items"][0]["id"] == observation_id
    assert dispatch_api(database, f"/api/v1/observations/{observation_id}", "")["status"] == "completed"
    assert dispatch_api(database, f"/api/v1/observations/{observation_id}/analysis", "")["configuration"]["id"] == observation_id
    immutable = dispatch_api(database, f"/api/v1/observations/{observation_id}/results/{result_id}", "")
    assert immutable["configuration_hash"]
    assert "configuration_json" not in immutable


def test_render_contains_patient_language_local_asset_and_no_external_runtime(tmp_path: Path) -> None:
    database = synthetic_database(tmp_path)
    connection = sqlite3.connect(database)
    connection.row_factory = sqlite3.Row
    bundle = build_bundle(connection, today="2026-06-15")
    connection.close()
    html = render(bundle, explorer_6c=True, health_record_6e=True)
    assert "Meine Fragestellungen" in html
    assert "Aktive Beobachtung dokumentieren" in html
    assert "Dies ist eine persönliche, deskriptive Beobachtung. Sie zeigt keine Ursache oder medizinisch bestätigte Wirkung." in html
    assert "/health-assets/dashboard-v5-observations.js" in html
    assert "http://" not in html and "https://" not in html
    assert "__CSRF_TOKEN__" in html


def test_doctor_report_has_at_most_two_personal_observation_charts(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    database = synthetic_database(tmp_path)
    monkeypatch.setattr(worker, "DASHBOARD_DB", database)
    worker.apply_observation_action(question(status="active", include_doctor=True))
    connection = sqlite3.connect(database)
    observation_id = connection.execute("SELECT id FROM personal_observations").fetchone()[0]
    connection.close()
    worker.apply_observation_action(phase(observation_id, metrics=["nutrition.histamine", "apple.sleep", "apple.hrv"]))
    report = dispatch_api(database, "/api/v1/doctor-report", "from=2026-05-01&to=2026-06-15&sections=personal_observations")
    assert len(report["personal_observations"]) == 1
    assert len(report["personal_observations"][0]["charts"]) <= 2
    assert "Kausal" in report["personal_observations"][0]["medical_statement"] or "Ursache" in report["personal_observations"][0]["medical_statement"]
