from __future__ import annotations

import importlib.util
import json
from datetime import date, datetime, timedelta, timezone
from pathlib import Path

import pytest

from checkin_cli.nutrition_onboarding_finalization import validate_clinical_clearance
from checkin_cli.nutrition_readiness import audit_nutrition_start_readiness
from checkin_cli.nutrition_readiness_io import canonical_digest
from checkin_cli.nutrition_restriction_kb import (
    load_restriction_kb_template,
    seed_restriction_kb,
)

MODULE = "checkin_cli.nutrition_onboarding"


def _load():
    if importlib.util.find_spec(MODULE) is None:
        pytest.fail("nutrition onboarding service contract missing")
    return __import__(MODULE, fromlist=["*"])


def _authority(mod, **overrides: object):
    values: dict[str, object] = {
        "customer_key": "client_001",
        "customer_user_id": 10,
        "customer_chat_id": -100,
        "customer_topic_id": 20,
        "owner_user_id": 12,
        "owner_chat_id": -100,
        "owner_topic_id": 22,
        "consent_notice_version": "privacy-v1",
        "consent_granted": True,
        "customer_enabled": False,
    }
    values.update(overrides)
    return mod.OnboardingAuthority(**values)


def _evidence(mod, *, actor: int = 10, topic: int = 20, message: int = 1):
    return mod.MessageEvidence(
        actor_user_id=actor,
        chat_id=-100,
        topic_id=topic,
        message_id=message,
        update_id=message + 100,
    )


def test_service_contract_is_present() -> None:
    mod = _load()
    assert mod.NUTRITION_ONBOARDING_API_VERSION == "2.0"
    assert len(mod.QUESTION_FIELDS) >= 20


def test_start_rejects_symlinked_profile_data_ancestor(tmp_path: Path) -> None:
    mod = _load()
    profile_root = tmp_path / "profile"
    outside = tmp_path / "outside"
    profile_root.mkdir()
    outside.mkdir()
    (profile_root / "data").symlink_to(outside, target_is_directory=True)

    with pytest.raises(ValueError, match="symlink"):
        mod.NutritionOnboardingService(
            profile_root=profile_root,
            customer_key="client_001",
            enforce_current_authority=False,
        )
    assert not (outside / "customers" / "client_001").exists()


def test_start_rejects_symlinked_profile_root(tmp_path: Path) -> None:
    mod = _load()
    outside = tmp_path / "outside"
    outside.mkdir()
    profile_root = tmp_path / "profile"
    profile_root.symlink_to(outside, target_is_directory=True)

    with pytest.raises(ValueError, match="profile root"):
        mod.NutritionOnboardingService(
            profile_root=profile_root,
            customer_key="client_001",
            enforce_current_authority=False,
        )
    assert not (outside / "data").exists()


def test_start_rejects_profile_data_swapped_to_symlink_after_construction(
    tmp_path: Path,
) -> None:
    mod = _load()
    profile_root = tmp_path / "profile"
    outside = tmp_path / "outside"
    (profile_root / "data").mkdir(parents=True)
    outside.mkdir()
    service = mod.NutritionOnboardingService(
        profile_root=profile_root,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    data_path = profile_root / "data"
    data_path.rename(profile_root / "data-original")
    data_path.symlink_to(outside, target_is_directory=True)

    with pytest.raises(ValueError, match="symlink"):
        service.store.initialize()
    with pytest.raises(ValueError, match="symlink"):
        service.start_or_resume(
            authority=_authority(mod),
            evidence=_evidence(mod),
        )
    assert not (outside / "customers" / "client_001").exists()


def test_start_requires_current_privacy_consent_and_disabled_target(tmp_path: Path) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )

    with pytest.raises(ValueError, match="privacy-v1"):
        service.start_or_resume(
            authority=_authority(mod, consent_granted=False),
            evidence=_evidence(mod),
        )
    with pytest.raises(ValueError, match="disabled"):
        service.start_or_resume(
            authority=_authority(mod, customer_enabled=True),
            evidence=_evidence(mod),
        )


def test_service_rejects_mutation_without_current_registry_authority(
    tmp_path: Path,
) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
    )

    with pytest.raises(ValueError, match="authority"):
        service.start_or_resume(
            authority=_authority(mod),
            evidence=_evidence(mod),
        )
    assert not service.session_path.exists()


def test_customer_answers_require_exact_route_and_question_order(tmp_path: Path) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    authority = _authority(mod)
    service.start_or_resume(authority=authority, evidence=_evidence(mod))

    with pytest.raises(ValueError, match="route"):
        service.submit_answer(
            field=mod.QUESTION_FIELDS[0],
            value="1996-08-01",
            authority=authority,
            evidence=_evidence(mod, actor=999),
        )
    with pytest.raises(ValueError, match="expected question"):
        service.submit_answer(
            field=mod.QUESTION_FIELDS[1],
            value="male",
            authority=authority,
            evidence=_evidence(mod, message=2),
        )


def test_restart_preserves_answer_cursor_without_exposing_values(tmp_path: Path) -> None:
    mod = _load()
    authority = _authority(mod)
    first = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    first.start_or_resume(authority=authority, evidence=_evidence(mod))
    first.submit_answer(
        field=mod.QUESTION_FIELDS[0],
        value="1996-08-01",
        authority=authority,
        evidence=_evidence(mod, message=2),
    )

    restarted = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    status = restarted.status()
    assert status.answer_count == 1
    assert status.next_field == mod.QUESTION_FIELDS[1]
    assert "1996-08-01" not in status.model_dump_json()


def test_attestation_goes_directly_to_owner_review(tmp_path: Path) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    authority = _authority(mod)
    service.start_or_resume(authority=authority, evidence=_evidence(mod))
    for index, field in enumerate(mod.QUESTION_FIELDS):
        service.submit_answer(
            field=field,
            value=mod.example_answer(field),
            authority=authority,
            evidence=_evidence(mod, message=index + 2),
        )

    status = service.status()
    assert status.state == mod.OnboardingState.CUSTOMER_ATTESTATION
    attested = service.attest_baseline(
        authority=authority,
        evidence=_evidence(mod, message=99),
    )
    assert attested.state == mod.OnboardingState.OWNER_REVIEW

    owned = service.review_as_owner(
        decision="approved",
        authority=authority,
        evidence=_evidence(mod, actor=12, topic=22, message=100),
    )
    assert owned.state == mod.OnboardingState.FINALIZING


def test_reconciliation_answers_are_authorized_and_copy_only(
    tmp_path: Path,
) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    authority = _authority(mod)
    service.start_or_resume(authority=authority, evidence=_evidence(mod))
    expected = {}
    for index, field in enumerate(mod.QUESTION_FIELDS):
        value = mod.example_answer(field)
        expected[field] = value
        service.submit_answer(
            field=field,
            value=value,
            authority=authority,
            evidence=_evidence(mod, message=index + 2),
        )

    answers = service.reconciliation_answers(authority=authority)

    assert answers == expected
    answers["meal_count"] = 99
    assert service.reconciliation_answers(authority=authority) == expected
    with pytest.raises(ValueError, match="customer attestation"):
        service.attest_baseline(
            authority=authority,
            evidence=_evidence(mod, message=99),
        )
        service.reconciliation_answers(authority=authority)


def test_reconciliation_revision_is_digest_bound_and_customer_authorized(
    tmp_path: Path,
) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    authority = _authority(mod)
    service.start_or_resume(authority=authority, evidence=_evidence(mod))
    original = {}
    for index, field in enumerate(mod.QUESTION_FIELDS):
        value = mod.example_answer(field)
        original[field] = value
        service.submit_answer(
            field=field,
            value=value,
            authority=authority,
            evidence=_evidence(mod, message=index + 2),
        )
    answers_digest = canonical_digest(original)

    revised = service.revise_reconciliation_answer(
        field="meal_count",
        value=5,
        expected_answers_digest=answers_digest,
        authority=authority,
        evidence=_evidence(mod, message=99),
    )

    assert revised.state == mod.OnboardingState.CUSTOMER_ATTESTATION
    expected = {**original, "meal_count": 5}
    assert service.reconciliation_answers(authority=authority) == expected
    with pytest.raises(ValueError, match="digest"):
        service.revise_reconciliation_answer(
            field="meal_count",
            value=6,
            expected_answers_digest=answers_digest,
            authority=authority,
            evidence=_evidence(mod, message=100),
        )
    with pytest.raises(ValueError, match="canonical"):
        service.revise_reconciliation_answer(
            field="not_a_field",
            value="unsafe",
            expected_answers_digest=canonical_digest(expected),
            authority=authority,
            evidence=_evidence(mod, message=101),
        )
    assert service.reconciliation_answers(authority=authority) == expected


def test_redacted_status_document_contains_no_answer_values(tmp_path: Path) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    authority = _authority(mod)
    service.start_or_resume(authority=authority, evidence=_evidence(mod))
    service.submit_answer(
        field=mod.QUESTION_FIELDS[0],
        value="1996-08-01",
        authority=authority,
        evidence=_evidence(mod, message=2),
    )

    rendered = json.dumps(service.status().model_dump(mode="json"), sort_keys=True)
    assert "1996-08-01" not in rendered
    assert "answers" not in rendered


def test_safety_hold_requires_scoped_owner_clinical_clearance(tmp_path: Path) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    authority = _authority(mod)
    service.start_or_resume(authority=authority, evidence=_evidence(mod))
    for index, field in enumerate(mod.QUESTION_FIELDS):
        value = mod.example_answer(field)
        if field == "conditions":
            value = {"status": "provided", "items": ["외부 검토 필요"]}
        service.submit_answer(
            field=field,
            value=value,
            authority=authority,
            evidence=_evidence(mod, message=index + 2),
        )
    held = service.attest_baseline(
        authority=authority,
        evidence=_evidence(mod, message=99),
    )
    assert held.state == mod.OnboardingState.SAFETY_HOLD

    cleared = service.record_clinical_review(
        cleared=True,
        external_reference="opaque-review-001",
        authority=authority,
        evidence=_evidence(mod, actor=12, topic=22, message=100),
    )
    assert cleared.state == mod.OnboardingState.OWNER_REVIEW

    clinical_path = service.root / "clinical-review.json"
    clinical = json.loads(clinical_path.read_text(encoding="utf-8"))
    assert validate_clinical_clearance(
        clinical_path,
        baseline_digest=str(clinical["baseline_digest"]),
    ) == clinical

    for field, value in (
        ("cleared_for_nonmedical_coaching", False),
        ("baseline_digest", "f" * 64),
    ):
        tampered = dict(clinical)
        tampered[field] = value
        tampered["digest"] = canonical_digest(tampered)
        clinical_path.write_text(json.dumps(tampered), encoding="utf-8")
        with pytest.raises(ValueError, match="clinical clearance"):
            validate_clinical_clearance(
                clinical_path,
                baseline_digest=str(clinical["baseline_digest"]),
            )


def test_cancel_purges_answers_and_leaves_redacted_tombstone(tmp_path: Path) -> None:
    mod = _load()
    service = mod.NutritionOnboardingService(
        profile_root=tmp_path,
        customer_key="client_001",
        enforce_current_authority=False,
    )
    authority = _authority(mod)
    service.start_or_resume(authority=authority, evidence=_evidence(mod))
    service.submit_answer(
        field=mod.QUESTION_FIELDS[0],
        value="1996-08-01",
        authority=authority,
        evidence=_evidence(mod, message=2),
    )

    cancelled = service.cancel(
        authority=authority,
        evidence=_evidence(mod, message=3),
    )
    assert cancelled.state == mod.OnboardingState.CANCELLED
    assert cancelled.answer_count == 0
    assert cancelled.next_field is None
    assert "1996-08-01" not in service.session_path.read_text(encoding="utf-8")


def test_owner_finalization_writes_ready_artifacts_and_disabled_projection(
    tmp_path: Path,
) -> None:
    mod = _load()
    profile_root = tmp_path / "profile"
    profile_root.mkdir(mode=0o700)
    source = tmp_path / "kb-template.json"
    source.write_text(json.dumps(load_restriction_kb_template()), encoding="utf-8")
    seed_restriction_kb(
        profile_root=profile_root,
        source=source,
        owner_digest="a" * 64,
        commit=True,
        as_of=date(2026, 8, 1),
    )
    (profile_root / "data").chmod(0o700)
    registry_path = profile_root / "customers" / "registry.json"
    registry_path.parent.mkdir(parents=True)
    registry_path.write_text(
        json.dumps(
            {
                "customers": [
                    {
                        "customer_key": "client_001",
                        "display_name": "Client",
                        "enabled": False,
                        "routes": {"customer": "opaque", "owner": "opaque"},
                        "consent": {"privacy-v1": "granted"},
                        "nutrition_profile": {"status": "draft"},
                        "plan": {"weeks": []},
                    }
                ]
            }
        ),
        encoding="utf-8",
    )
    authority = _authority(mod)
    service = mod.NutritionOnboardingService(
        profile_root=profile_root,
        customer_key="client_001",
        enforce_current_authority=False,
        enforce_reconciliation=True,
    )
    service.start_or_resume(authority=authority, evidence=_evidence(mod))
    answers = {}
    for index, field in enumerate(mod.QUESTION_FIELDS):
        value = mod.example_answer(field)
        if field == "conditions":
            value = {"status": "provided", "items": ["외부 검토 필요"]}
        answers[field] = value
        service.submit_answer(
            field=field,
            value=value,
            authority=authority,
            evidence=_evidence(mod, message=index + 2),
        )
    reconciliation = service.record_reconciliation(
        answers_digest=canonical_digest(answers),
        advisory={"summary_ko": "입력 내용을 확인했습니다."},
        clarifications=[],
        authority=authority,
    )
    reconciliation_digest = str(reconciliation["digest"])
    held = service.attest_baseline(
        authority=authority,
        evidence=_evidence(mod, message=99),
    )
    assert held.state == mod.OnboardingState.SAFETY_HOLD
    service.record_clinical_review(
        cleared=True,
        external_reference="opaque-review-finalization",
        authority=authority,
        evidence=_evidence(mod, actor=12, topic=22, message=100),
    )
    service.review_as_owner(
        decision="approved",
        authority=authority,
        evidence=_evidence(mod, actor=12, topic=22, message=101),
    )

    ready = service.finalize(
        starts_on=date(2026, 8, 3),
        issued_at_kst=datetime(
            2026,
            8,
            1,
            3,
            tzinfo=timezone(timedelta(hours=9)),
        ),
        privacy_consent_digest="b" * 64,
        feature_epoch_digest="c" * 64,
    )

    assert ready.state == mod.OnboardingState.READY
    audit = audit_nutrition_start_readiness(
        profile_root,
        "client_001",
        now_kst=datetime(2026, 8, 1, 4, tzinfo=timezone(timedelta(hours=9))),
    )
    assert audit.ready is True
    assert audit.checks["pointer"] is True
    baseline = json.loads(
        (service.root / "baseline-v1.json").read_text(encoding="utf-8")
    )
    receipt = json.loads(
        (service.root / "readiness-receipt-v1.json").read_text(encoding="utf-8")
    )
    pointer = json.loads(
        (service.root / "readiness-current.json").read_text(encoding="utf-8")
    )
    ready_document = json.loads(service.ready_path.read_text(encoding="utf-8"))
    for document in (baseline, receipt, pointer, ready_document):
        assert document["input_reconciliation_digest"] == reconciliation_digest

    clinical_path = service.root / "clinical-review.json"
    clinical_text = clinical_path.read_text(encoding="utf-8")
    clinical_path.unlink()
    missing_clinical = audit_nutrition_start_readiness(
        profile_root,
        "client_001",
        now_kst=datetime(2026, 8, 1, 4, tzinfo=timezone(timedelta(hours=9))),
    )
    assert missing_clinical.ready is False
    assert "baseline_invalid" in missing_clinical.reason_codes
    clinical_path.write_text(clinical_text, encoding="utf-8")
    clinical_path.chmod(0o600)

    pointer_path = service.root / "readiness-current.json"
    pointer_text = pointer_path.read_text(encoding="utf-8")
    pointer = json.loads(pointer_text)
    pointer_path.unlink()
    missing_pointer = audit_nutrition_start_readiness(
        profile_root,
        "client_001",
        now_kst=datetime(2026, 8, 1, 4, tzinfo=timezone(timedelta(hours=9))),
    )
    assert missing_pointer.ready is False
    assert "readiness_receipt_invalid" in missing_pointer.reason_codes
    pointer_path.write_text(pointer_text, encoding="utf-8")

    pointer["bundle_digest"] = "f" * 64
    pointer["digest"] = canonical_digest(pointer)
    pointer_path.write_text(json.dumps(pointer), encoding="utf-8")
    tampered = audit_nutrition_start_readiness(
        profile_root,
        "client_001",
        now_kst=datetime(2026, 8, 1, 4, tzinfo=timezone(timedelta(hours=9))),
    )
    assert tampered.ready is False
    assert "readiness_receipt_invalid" in tampered.reason_codes
    registry = json.loads(registry_path.read_text(encoding="utf-8"))
    assert registry["customers"][0]["enabled"] is False
    assert len(registry["customers"][0]["plan"]["weeks"]) == 12
    assert not service.session_path.exists()
