"""Synthetic compatibility coverage for nutrition-daily v1 draft and binding state."""

from __future__ import annotations

import json
import sys
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo

import pytest
from pydantic import ValidationError


ROOT = Path(__file__).resolve().parents[2]
PROFILE_PACKAGE = ROOT / "dualcoach" / "profile"
for source_path in (ROOT, PROFILE_PACKAGE):
    source_text = str(source_path)
    if source_text not in sys.path:
        sys.path.insert(0, source_text)

from checkin_cli.wizard import WizardService
from checkin_cli.wizard_models import WizardContext, WizardFlow, WizardSession, WizardStatus
from gateway.platforms.physique_checkin import CallbackData, PhysiqueCheckinBridge
from gateway.platforms.physique_checkin_bindings import BindingStoreCorruption
from gateway.platforms.physique_checkin_config import PhysiqueCheckinConfig


NOW = 4_000_000_000
SESSION = "f1e2d3c4b5a697887766554433221100"
OWNER = "opaque-owner-v1"
CHAT = "opaque-chat-v1"
TOPIC = "opaque-topic-v1"
MESSAGE = "opaque-message-v1"
BODYWEIGHT = "61.4"
CALORIES = "2460"


def _kst_day() -> str:
    return datetime.now(ZoneInfo("Asia/Seoul")).date().isoformat()


def _config() -> PhysiqueCheckinConfig:
    return PhysiqueCheckinConfig(OWNER, CHAT, TOPIC, 3_600, False, False)


def _legacy_session(
    *,
    step: str = "calories",
    version: int = 1,
    answers: dict[str, str] | None = None,
) -> dict[str, object]:
    session = WizardSession(
        session_id=SESSION,
        flow=WizardFlow.NUTRITION,
        owner_id=OWNER,
        customer_key=None,
        topic_id=TOPIC,
        kst_day=_kst_day(),
        version=version,
        step=step,
        answers=answers or {"bodyweight": BODYWEIGHT},
        state_schema_version=1,
    )
    # ``unknown_steps`` and cursor history did not exist in the v1 bytes.
    return session.model_dump(
        mode="json",
        exclude={"unknown_steps", "step_history"},
    )


def _legacy_binding(*, step: str = "calories", version: int = 1, expires_at: int = NOW + 3_600) -> dict[str, object]:
    return {
        "version": 1,
        "active_session_id": SESSION,
        "bindings": [{
            "session_id": SESSION,
            "owner_id": OWNER,
            "chat_id": CHAT,
            "topic_id": TOPIC,
            "step": step,
            "version": version,
            "message_id": MESSAGE,
            "expires_at": expires_at,
        }],
    }


def _write_legacy_state(
    root: Path,
    *,
    session: dict[str, object] | None = None,
    binding: dict[str, object] | None = None,
) -> tuple[Path, Path]:
    wizard_root = root / "wizard"
    drafts = wizard_root / "drafts"
    _ = drafts.mkdir(parents=True, mode=0o700)
    draft_path = drafts / f"{SESSION}.json"
    _ = draft_path.write_text(
        json.dumps(session or _legacy_session(), separators=(",", ":"), sort_keys=True),
        encoding="utf-8",
    )
    _ = draft_path.chmod(0o600)
    binding_path = root / "bindings.json"
    _ = binding_path.write_text(
        json.dumps(binding or _legacy_binding(), separators=(",", ":"), sort_keys=True),
        encoding="utf-8",
    )
    _ = binding_path.chmod(0o600)
    return wizard_root, binding_path


def _bridge(root: Path) -> tuple[PhysiqueCheckinBridge, WizardService, Path, Path]:
    wizard_root, binding_path = _write_legacy_state(root)
    service = WizardService.for_standalone(wizard_root)
    bridge = PhysiqueCheckinBridge(_config(), service=service, binding_path=binding_path)
    return bridge, service, wizard_root, binding_path


def _session(wizard_root: Path) -> WizardSession:
    return WizardSession.model_validate_json(
        (wizard_root / "drafts" / f"{SESSION}.json").read_text(encoding="utf-8")
    )


def _callback(bridge: PhysiqueCheckinBridge, action: str) -> str:
    prompt = bridge.active_prompt()
    assert prompt is not None
    for row in prompt.button_rows:
        for _label, data in row:
            parsed = CallbackData.parse(data)
            if parsed is not None and parsed.action == action:
                return data
    raise AssertionError(f"missing {action!r} callback")


def test_characterization_v1_calories_binding_load_preserves_bytes_cursor_answer_and_message(
    tmp_path: Path,
) -> None:
    wizard_root, binding_path = _write_legacy_state(tmp_path)
    draft_before = (wizard_root / "drafts" / f"{SESSION}.json").read_bytes()
    binding_before = binding_path.read_bytes()

    bridge = PhysiqueCheckinBridge(
        _config(),
        service=WizardService.for_standalone(wizard_root),
        binding_path=binding_path,
    )

    assert b'"unknown_steps"' not in draft_before
    assert _session(wizard_root).unknown_steps == ()
    assert (wizard_root / "drafts" / f"{SESSION}.json").read_bytes() == draft_before
    assert binding_path.read_bytes() == binding_before
    cursor = bridge.active_cursor_identity(now_epoch=NOW)
    assert cursor is not None
    assert (cursor[0].session_id, cursor[0].step, cursor[0].version) == (
        SESSION,
        "calories",
        1,
    )
    assert bridge.active_prompt_message_id() == MESSAGE
    snapshot = bridge.active_checkin_snapshot()
    assert snapshot is not None and snapshot["answers"] == {"bodyweight": BODYWEIGHT}
    prompt = bridge.active_prompt()
    assert prompt is not None and prompt.text.startswith("진행 2/12")


@pytest.mark.parametrize("action", ("known", "unknown"))
def test_v1_calories_transition_advances_once_to_current_v2_shape(
    tmp_path: Path,
    action: str,
) -> None:
    bridge, service, wizard_root, _binding_path = _bridge(tmp_path)
    prompt = bridge.active_prompt()
    assert prompt is not None and prompt.text.startswith("진행 2/12")

    if action == "known":
        reply = bridge.handle_text(CALORIES, OWNER, CHAT, TOPIC, now_epoch=NOW)
    else:
        reply = bridge.handle_callback(
            _callback(bridge, "u"), OWNER, CHAT, TOPIC, MESSAGE, now_epoch=NOW,
        )

    assert reply is not None and reply.accepted
    cursor = bridge.active_cursor_identity(now_epoch=NOW)
    assert cursor is not None and (cursor[0].step, cursor[0].version) == ("macros", 2)
    assert reply.prompt is not None and "진행 3/12" in reply.prompt.text
    persisted = _session(wizard_root)
    assert (persisted.step, persisted.version, persisted.state_schema_version) == (
        "macros",
        2,
        2,
    )
    assert persisted.answers["bodyweight"] == BODYWEIGHT
    if action == "known":
        assert persisted.answers["calories"] == CALORIES
        assert persisted.unknown_steps == ()
    else:
        assert "calories" not in persisted.answers
        assert persisted.unknown_steps == ("calories",)

    stale = service.answer(
        WizardContext(OWNER, TOPIC), SESSION, 1, "unknown",
    )
    assert stale.status is WizardStatus.REJECTED
    assert _session(wizard_root) == persisted


def test_duplicate_v1_callback_cannot_advance_or_rewrite_a_second_time(tmp_path: Path) -> None:
    bridge, _service, wizard_root, binding_path = _bridge(tmp_path)
    callback = _callback(bridge, "u")

    accepted = bridge.handle_callback(
        callback, OWNER, CHAT, TOPIC, MESSAGE, now_epoch=NOW,
    )
    assert accepted.accepted
    draft_after_first = (wizard_root / "drafts" / f"{SESSION}.json").read_bytes()
    binding_after_first = binding_path.read_bytes()

    replay = bridge.handle_callback(
        callback, OWNER, CHAT, TOPIC, MESSAGE, now_epoch=NOW,
    )

    assert not replay.accepted
    cursor = bridge.active_cursor_identity(now_epoch=NOW)
    assert cursor is not None and (cursor[0].step, cursor[0].version) == ("macros", 2)
    assert (wizard_root / "drafts" / f"{SESSION}.json").read_bytes() == draft_after_first
    assert binding_path.read_bytes() == binding_after_first


def test_startup_only_keeps_future_v1_binding_addressable_without_events_or_replacement_draft(
    tmp_path: Path,
) -> None:
    wizard_root, binding_path = _write_legacy_state(tmp_path)
    draft_path = wizard_root / "drafts" / f"{SESSION}.json"
    draft_before = draft_path.read_bytes()
    binding_before = binding_path.read_bytes()
    events_path = wizard_root / "events.jsonl"

    bridge = PhysiqueCheckinBridge(
        _config(),
        service=WizardService.for_standalone(wizard_root),
        binding_path=binding_path,
    )

    assert bridge.cursor_identity(SESSION, now_epoch=NOW) is not None
    repeated = PhysiqueCheckinBridge(
        _config(),
        service=WizardService.for_standalone(wizard_root),
        binding_path=binding_path,
    )
    assert repeated.cursor_identity(SESSION, now_epoch=NOW) is not None
    assert draft_path.read_bytes() == draft_before
    assert binding_path.read_bytes() == binding_before
    assert not events_path.exists()
    assert tuple((wizard_root / "drafts").glob("*.json")) == (draft_path,)


def test_expired_v1_binding_is_observation_only_until_explicit_resume(tmp_path: Path) -> None:
    expired_at = 1
    wizard_root, binding_path = _write_legacy_state(
        tmp_path,
        binding=_legacy_binding(expires_at=expired_at),
    )
    draft_path = wizard_root / "drafts" / f"{SESSION}.json"
    draft_before = draft_path.read_bytes()
    binding_before = binding_path.read_bytes()
    service = WizardService.for_standalone(wizard_root)
    bridge = PhysiqueCheckinBridge(_config(), service=service, binding_path=binding_path)

    assert bridge.active_cursor_identity() is None
    assert bridge.active_prompt() is None
    assert draft_path.read_bytes() == draft_before
    assert binding_path.read_bytes() == binding_before
    assert not (wizard_root / "events.jsonl").exists()

    launcher = bridge.open_launcher(
        "nutrition_daily", message_id="opaque-explicit-resume", now_epoch=NOW,
    )
    assert launcher.accepted and launcher.callback_data is not None
    resumed = bridge.handle_callback(
        launcher.callback_data,
        OWNER,
        CHAT,
        TOPIC,
        "opaque-explicit-resume",
        now_epoch=NOW,
    )
    assert resumed.accepted
    cursor = bridge.active_cursor_identity(now_epoch=NOW)
    assert cursor is not None and cursor[0].session_id == SESSION


def test_corrupt_contradictory_and_mismatched_candidates_do_not_mutate_domain_state(
    tmp_path: Path,
) -> None:
    corrupt_path = tmp_path / "corrupt" / "bindings.json"
    _ = corrupt_path.parent.mkdir(mode=0o700)
    _ = corrupt_path.write_text("{", encoding="utf-8")
    _ = corrupt_path.chmod(0o600)
    with pytest.raises(BindingStoreCorruption):
        _ = PhysiqueCheckinBridge(
            _config(),
            service=WizardService.for_standalone(tmp_path / "corrupt" / "wizard"),
            binding_path=corrupt_path,
        )

    valid_v2 = _legacy_session(
        step="macros",
        version=2,
        answers={"bodyweight": BODYWEIGHT, "calories": CALORIES},
    )
    valid_v2["state_schema_version"] = 2
    valid_v2["unknown_steps"] = ["calories"]
    valid_v2["step_history"] = ["bodyweight", "calories"]
    contradictory_root = tmp_path / "contradictory"
    wizard_root, _binding_path = _write_legacy_state(
        contradictory_root,
        session=valid_v2,
        binding=_legacy_binding(step="macros", version=2),
    )
    raw = (wizard_root / "drafts" / f"{SESSION}.json").read_bytes()
    with pytest.raises(ValidationError):
        _ = WizardSession.model_validate_json(raw)
    assert (wizard_root / "drafts" / f"{SESSION}.json").read_bytes() == raw
    assert not (wizard_root / "events.jsonl").exists()

    mismatched_root = tmp_path / "mismatched"
    bridge, _service, mismatch_wizard, mismatch_binding = _bridge(mismatched_root)
    _ = mismatch_binding.write_text(
        json.dumps(_legacy_binding(step="macros", version=2), separators=(",", ":")),
        encoding="utf-8",
    )
    _ = mismatch_binding.chmod(0o600)
    bridge = PhysiqueCheckinBridge(
        _config(),
        service=WizardService.for_standalone(mismatch_wizard),
        binding_path=mismatch_binding,
    )
    draft_before = (mismatch_wizard / "drafts" / f"{SESSION}.json").read_bytes()
    reply = bridge.handle_text("180 120 50", OWNER, CHAT, TOPIC, now_epoch=NOW)
    assert reply is not None and not reply.accepted
    assert (mismatch_wizard / "drafts" / f"{SESSION}.json").read_bytes() == draft_before
    assert not (mismatch_wizard / "events.jsonl").exists()


def test_v1_free_text_is_not_reinterpreted_as_an_explicit_unknown(tmp_path: Path) -> None:
    legacy_answers = {
        "bodyweight": BODYWEIGHT,
        "calories": CALORIES,
        "macros": "180 120 50",
        "meals": "unknown",
    }
    wizard_root, binding_path = _write_legacy_state(
        tmp_path,
        session=_legacy_session(step="water", version=4, answers=legacy_answers),
        binding=_legacy_binding(step="water", version=4),
    )
    bridge = PhysiqueCheckinBridge(
        _config(),
        service=WizardService.for_standalone(wizard_root),
        binding_path=binding_path,
    )

    reply = bridge.handle_text("2.0", OWNER, CHAT, TOPIC, now_epoch=NOW)

    assert reply is not None and reply.accepted
    persisted = _session(wizard_root)
    assert persisted.state_schema_version == 2
    assert persisted.answers["meals"] == "unknown"
    assert persisted.unknown_steps == ()
