"""
Cross-seam integration tests for the deterministic core (Group 5, task 5.1/5.2).

Exercises scanner + state_store + reminder together on a small fixture doc tree
built in a tmp dir, verifying the end-to-end deterministic path:

  - the scanner produces a shortlist (and self-excludes .cc-os/dochygiene/),
  - the state store records timestamps and rolls reports over (keeps one pair),
  - the reminder's snooze behaves across simulated sessions with an injected
    clock (silent when fresh; banner when stale; snoozed same calendar day;
    re-banners the next day),

all with NO model and NO network — the deterministic-first invariant (#6).

sys.path is patched here (no shared conftest) so the file is self-contained.
"""

from __future__ import annotations

import json
import sys
from datetime import datetime, timezone
from io import StringIO
from pathlib import Path

import pytest

_SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))

from scanner import Scanner  # noqa: E402
from state_store import StateStore, resolve_project_root  # noqa: E402
from reminder import Decision, ReminderRunner  # noqa: E402


def _utc(y, m, d, hh=0, mm=0):
    return datetime(y, m, d, hh, mm, tzinfo=timezone.utc)


class FrozenClock:
    def __init__(self, now):
        self._now = now

    def now(self):
        return self._now


def _build_doc_tree(root: Path) -> None:
    """A minimal project: a git marker, two docs, plus a frozen + ignored doc."""
    (root / ".git").mkdir()
    (root / "CLAUDE.md").write_text("# Project\nSee scripts/missing.py for details.\n")
    (root / "docs").mkdir()
    (root / "docs" / "plan.md").write_text("# Plan\nstuff\n")
    # frozen doc — must NOT appear in shortlist (invariant #9)
    (root / "docs" / "frozen.md").write_text("---\nhygiene: frozen\n---\n# Frozen\n")
    # ignored doc
    (root / ".dochygiene-ignore").write_text("docs/secret.md\n")
    (root / "docs" / "secret.md").write_text("# Secret\n")


class TestScannerStateSeam:

    def test_scan_shortlist_and_self_exclusion(self, tmp_path):
        _build_doc_tree(tmp_path)
        # Resolve root exactly as production would (git root).
        root = resolve_project_root(tmp_path / "docs")
        assert root == tmp_path

        # The state store writes its dir first; the scanner must self-exclude it.
        store = StateStore(root)
        store.set_timestamp("last_check", _utc(2026, 6, 1))
        assert (root / ".cc-os" / "dochygiene" / "state.json").exists()

        artifact = Scanner(root=root, git_log_fn=lambda p: []).run()

        shortlist = set(artifact["shortlist"])
        assert "CLAUDE.md" in shortlist
        assert "docs/plan.md" in shortlist
        # Exclusions honoured.
        assert "docs/frozen.md" not in shortlist
        assert "docs/secret.md" not in shortlist
        # .cc-os/dochygiene/ never scanned (self-exclusion).
        assert not any(p.startswith(".cc-os") for p in shortlist)

    def test_report_rollover_keeps_one_pair(self, tmp_path):
        _build_doc_tree(tmp_path)
        root = resolve_project_root(tmp_path)
        store = StateStore(root)

        store.write_report('{"v":1}', "# v1")
        store.write_report('{"v":2}', "# v2")

        state_dir = root / ".cc-os" / "dochygiene"
        jsons = list(state_dir.glob("report*.json"))
        mds = list(state_dir.glob("report*.md"))
        assert len(jsons) == 1
        assert len(mds) == 1
        blob = store.read_report()
        assert blob is not None
        assert json.loads(blob[0]) == {"v": 2}


class TestReminderAcrossSessions:
    """Drive the reminder over a sequence of simulated SessionStart events."""

    def _session(self, store, now) -> Decision:
        out = StringIO()
        decision = ReminderRunner(store, clock=FrozenClock(now), out=out).run()
        # Banner ⇒ systemMessage on stdout; silent ⇒ nothing.
        if decision is Decision.BANNER:
            assert "systemMessage" in json.loads(out.getvalue())
        else:
            assert out.getvalue() == ""
        return decision

    def test_full_lifecycle(self, tmp_path):
        _build_doc_tree(tmp_path)
        root = resolve_project_root(tmp_path)
        store = StateStore(root)

        # 1. Fresh check today → reminder is SILENT.
        store.set_timestamp("last_check", _utc(2026, 6, 24))
        assert self._session(store, _utc(2026, 6, 24, 10)) is Decision.SILENT

        # 2. Simulate the check ageing past the threshold (set last_check far back).
        store.set_timestamp("last_check", _utc(2026, 5, 1))

        # 3. First stale session → BANNER, records last_reminded.
        assert self._session(store, _utc(2026, 6, 24, 9)) is Decision.BANNER
        first_reminded = store.get_timestamp("last_reminded")
        assert first_reminded == _utc(2026, 6, 24, 9)

        # 4. Second session SAME calendar day (e.g. resume) → SILENT (snooze).
        assert self._session(store, _utc(2026, 6, 24, 18)) is Decision.SILENT
        assert store.get_timestamp("last_reminded") == first_reminded  # not bumped

        # 5. Next calendar day, still stale → BANNER again, bumps last_reminded.
        assert self._session(store, _utc(2026, 6, 25, 8)) is Decision.BANNER
        assert store.get_timestamp("last_reminded") == _utc(2026, 6, 25, 8)

        # 6. A fresh check clears staleness → SILENT regardless of snooze.
        store.set_timestamp("last_check", _utc(2026, 6, 25, 12))
        assert self._session(store, _utc(2026, 6, 25, 13)) is Decision.SILENT
