"""Tests for the os-status hooks. Run: python3 tests/hook_test.py"""

import importlib.util
import io
import json
import os
import subprocess
import sys
import unittest
from datetime import date
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest import mock

PLUGIN_ROOT = Path(__file__).resolve().parent.parent
REPO_PLUGINS = PLUGIN_ROOT.parent
sys.path.insert(0, str(PLUGIN_ROOT / "hooks"))

import checks  # noqa: E402
import session_start  # noqa: E402
from checks import (  # noqa: E402
    ABSENT_NOTE,
    CURRENT_CONFIG_VERSION,
    ENV_VAR,
    PRESENT_NOTE,
    TRACKER_ABSENT_NOTE,
    TRACKER_PLANKA_RETIRED_NOTE,
    Check,
    CheckResult,
    Ctx,
    adr_system_present,
    config_version_current,
    project_graph_present,
    run_all_json,
    subagent_model_env_override,
    tracker_configured,
    vault_hub_note_present,
)
from state import (  # noqa: E402
    StateDir,
    clear_snooze,
    find_project_root,
    read_config,
    write_config_value,
)

TODAY = date(2026, 7, 6)
YESTERDAY = date(2026, 7, 5)


def make_ctx(
    project_root=None,
    settings_path=Path("/nonexistent/settings.json"),
    vault_path=Path("/nonexistent/vault"),
    config=None,
    environ=None,
):
    return Ctx(
        project_root=project_root,
        settings_path=settings_path,
        vault_path=vault_path,
        config=config or {},
        environ=environ or {},
    )


def git_project(tmp):
    root = Path(tmp)
    (root / ".git").mkdir(parents=True)
    return root


class EnvOverrideCheckTest(unittest.TestCase):
    def test_ok_when_unset_everywhere(self):
        self.assertEqual("ok", subagent_model_env_override(make_ctx()).status)

    def test_warns_on_process_environment(self):
        result = subagent_model_env_override(make_ctx(environ={ENV_VAR: "haiku"}))
        self.assertEqual("warn", result.status)
        self.assertIn(ENV_VAR, result.message)
        self.assertIn("haiku", result.message)

    def test_warns_on_settings_env_block_naming_the_file(self):
        with TemporaryDirectory() as tmp:
            settings = Path(tmp) / "settings.json"
            settings.write_text(json.dumps({"env": {ENV_VAR: "haiku"}}))
            result = subagent_model_env_override(make_ctx(settings_path=settings))
            self.assertEqual("warn", result.status)
            self.assertIn(str(settings), result.message)
            self.assertIn("haiku", result.message)

    def test_names_both_sources_when_both_set(self):
        with TemporaryDirectory() as tmp:
            settings = Path(tmp) / "settings.json"
            settings.write_text(json.dumps({"env": {ENV_VAR: "haiku"}}))
            result = subagent_model_env_override(
                make_ctx(settings_path=settings, environ={ENV_VAR: "sonnet"})
            )
            self.assertIn("environment", result.message)
            self.assertIn(str(settings), result.message)

    def test_malformed_settings_is_ignored(self):
        with TemporaryDirectory() as tmp:
            settings = Path(tmp) / "settings.json"
            settings.write_text("{not json")
            self.assertEqual(
                "ok", subagent_model_env_override(make_ctx(settings_path=settings)).status
            )


class AdrCheckTest(unittest.TestCase):
    def test_present_notes_verbatim_wording(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            (root / "docs" / "adr").mkdir(parents=True)
            (root / "docs" / "adr" / "README.md").write_text("index")
            result = adr_system_present(make_ctx(project_root=root))
            self.assertEqual(CheckResult("note", PRESENT_NOTE), result)

    def test_dir_without_index_counts_as_absent(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            (root / "docs" / "adr").mkdir(parents=True)
            result = adr_system_present(make_ctx(project_root=root))
            self.assertEqual(CheckResult("warn", ABSENT_NOTE), result)

    def test_absent_warns_with_init_and_migrate(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            result = adr_system_present(make_ctx(project_root=root))
            self.assertEqual("warn", result.status)
            self.assertIn("/os-adr:init", result.message)
            self.assertIn("/os-adr:migrate", result.message)

    def test_legacy_os_adr_suppress_is_honored(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            (root / ".os-adr").mkdir()
            (root / ".os-adr" / "suppress").write_text("")
            self.assertEqual("ok", adr_system_present(make_ctx(project_root=root)).status)


class HubNoteCheckTest(unittest.TestCase):
    def vault_with(self, tmp, name, body):
        vault = Path(tmp) / "vault"
        vault.mkdir(exist_ok=True)
        (vault / name).write_text(body)
        return vault

    def hub_note(self, project):
        return (
            f"---\ntype: hub\ntags:\n  - type/hub\n  - project/{project}\n---\n# hub\n"
        )

    def test_missing_vault_is_silent(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, vault_path=Path(tmp) / "no-vault")
            self.assertEqual("ok", vault_hub_note_present(ctx).status)

    def test_facet_tag_scan_finds_hub(self):
        with TemporaryDirectory() as tmp:
            root = git_project(Path(tmp) / "myproj")
            vault = self.vault_with(tmp, "myproj-hub.md", self.hub_note("myproj"))
            ctx = make_ctx(project_root=root, vault_path=vault)
            self.assertEqual("ok", vault_hub_note_present(ctx).status)

    def test_no_hub_warns_naming_corrective_skill(self):
        with TemporaryDirectory() as tmp:
            root = git_project(Path(tmp) / "myproj")
            vault = self.vault_with(tmp, "other.md", self.hub_note("otherproj"))
            result = vault_hub_note_present(make_ctx(project_root=root, vault_path=vault))
            self.assertEqual("warn", result.status)
            self.assertIn("/os-vault:write", result.message)
            self.assertIn("myproj", result.message)

    def test_project_name_is_not_prefix_matched(self):
        with TemporaryDirectory() as tmp:
            root = git_project(Path(tmp) / "myproj")
            vault = self.vault_with(tmp, "other.md", self.hub_note("myproj2"))
            result = vault_hub_note_present(make_ctx(project_root=root, vault_path=vault))
            self.assertEqual("warn", result.status)

    def test_config_slug_wins_over_inference(self):
        with TemporaryDirectory() as tmp:
            root = git_project(Path(tmp) / "myproj")
            vault = self.vault_with(tmp, "custom-hub.md", "any content")
            ctx = make_ctx(
                project_root=root, vault_path=vault, config={"hub": "custom-hub"}
            )
            self.assertEqual("ok", vault_hub_note_present(ctx).status)

    def test_config_slug_pointing_nowhere_warns(self):
        with TemporaryDirectory() as tmp:
            root = git_project(Path(tmp) / "myproj")
            vault = self.vault_with(tmp, "other.md", "content")
            ctx = make_ctx(project_root=root, vault_path=vault, config={"hub": "ghost"})
            result = vault_hub_note_present(ctx)
            self.assertEqual("warn", result.status)
            self.assertIn("ghost", result.message)

    def test_templates_and_graphify_out_are_skipped(self):
        with TemporaryDirectory() as tmp:
            root = git_project(Path(tmp) / "myproj")
            vault = Path(tmp) / "vault"
            (vault / "_templates").mkdir(parents=True)
            (vault / "_templates" / "t.md").write_text(self.hub_note("myproj"))
            (vault / "graphify-out").mkdir()
            (vault / "graphify-out" / "g.md").write_text(self.hub_note("myproj"))
            result = vault_hub_note_present(make_ctx(project_root=root, vault_path=vault))
            self.assertEqual("warn", result.status)


class AuditDueCheckTest(unittest.TestCase):
    def ledger_env(self, tmp, rows):
        path = Path(tmp) / "metrics.tsv"
        header = "date\tsince\tsessions\n"
        path.write_text(header + "".join(f"{r}\t2026-01-01\t5\n" for r in rows))
        return {checks.AUDIT_LEDGER_ENV: str(path)}

    def test_missing_ledger_is_silent_machine_not_opted_in(self):
        ctx = make_ctx(environ={checks.AUDIT_LEDGER_ENV: "/nonexistent/metrics.tsv"})
        self.assertEqual("ok", checks.orchestration_audit_due(ctx).status)

    def test_recent_run_is_ok(self):
        with TemporaryDirectory() as tmp:
            env = self.ledger_env(tmp, [date.today().isoformat()])
            self.assertEqual("ok", checks.orchestration_audit_due(make_ctx(environ=env)).status)

    def test_stale_run_warns_with_last_date(self):
        with TemporaryDirectory() as tmp:
            env = self.ledger_env(tmp, ["2026-01-01", "2026-02-01"])
            result = checks.orchestration_audit_due(make_ctx(environ=env))
            self.assertEqual("warn", result.status)
            self.assertIn("2026-02-01", result.message)
            self.assertIn("/os-context:audit-sessions", result.message)

    def test_corrupt_ledger_reads_as_missing(self):
        with TemporaryDirectory() as tmp:
            path = Path(tmp) / "metrics.tsv"
            path.write_text("header only\nnot-a-date\tx\n")
            env = {checks.AUDIT_LEDGER_ENV: str(path)}
            self.assertEqual("ok", checks.orchestration_audit_due(make_ctx(environ=env)).status)


class TrackerCheckTest(unittest.TestCase):
    def test_absent_warns_pointing_to_routing_skill(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            result = tracker_configured(make_ctx(project_root=root))
            self.assertEqual(CheckResult("warn", TRACKER_ABSENT_NOTE), result)
            self.assertIn("/os-backlog:route", result.message)

    def test_planka_value_is_rejected_citing_adr_0042(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"tracker": "planka:my-board"})
            result = tracker_configured(ctx)
            self.assertEqual(CheckResult("warn", TRACKER_PLANKA_RETIRED_NOTE), result)
            self.assertIn("ADR-0042", result.message)
            self.assertIn("/os-backlog:route", result.message)

    def test_forgejo_value_is_ok(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"tracker": "forgejo:jared/cc-os"})
            self.assertEqual("ok", tracker_configured(ctx).status)

    def test_github_value_is_ok(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"tracker": "github:jared/cc-os"})
            self.assertEqual("ok", tracker_configured(ctx).status)

    def test_repo_value_is_ok(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"tracker": "repo:issues/"})
            self.assertEqual("ok", tracker_configured(ctx).status)

    def test_unknown_scheme_warns_without_crashing(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"tracker": "trello:board123"})
            result = tracker_configured(ctx)
            self.assertEqual("warn", result.status)
            self.assertIn("trello:board123", result.message)

    def test_missing_separator_warns_without_crashing(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"tracker": "bogus-my-board"})
            result = tracker_configured(ctx)
            self.assertEqual("warn", result.status)

    def test_forgejo_missing_repo_part_warns(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"tracker": "forgejo:jared"})
            result = tracker_configured(ctx)
            self.assertEqual("warn", result.status)

    def test_is_project_scoped_and_daily_snoozed_via_registry(self):
        entry = next(c for c in checks.REGISTRY if c.name == "tracker-configured")
        self.assertTrue(entry.project_scoped)


class ConfigVersionCheckTest(unittest.TestCase):
    def test_missing_version_warns(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            result = config_version_current(make_ctx(project_root=root))
            self.assertEqual("warn", result.status)
            self.assertIn("/os-status:fix", result.message)

    def test_current_version_is_ok(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"version": str(CURRENT_CONFIG_VERSION)})
            self.assertEqual("ok", config_version_current(ctx).status)

    def test_old_version_warns(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"version": "0"})
            self.assertEqual("warn", config_version_current(ctx).status)

    def test_non_numeric_version_warns_without_crashing(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root, config={"version": "banana"})
            result = config_version_current(ctx)
            self.assertEqual("warn", result.status)


class ProjectGraphCheckTest(unittest.TestCase):
    def test_missing_graph_warns_naming_onboard(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            result = project_graph_present(make_ctx(project_root=root))
            self.assertEqual("warn", result.status)
            self.assertIn("/os-vault:onboard-project", result.message)

    def test_present_graph_is_ok(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            (root / "graphify-out").mkdir()
            (root / "graphify-out" / "graph.json").write_text("{}")
            result = project_graph_present(make_ctx(project_root=root))
            self.assertEqual("ok", result.status)


class RemediationTest(unittest.TestCase):
    def test_every_registry_entry_has_a_remediation(self):
        for entry in checks.REGISTRY:
            self.assertTrue(entry.remediation, f"{entry.name} has no remediation")

    def test_known_remediations(self):
        by_name = {c.name: c.remediation for c in checks.REGISTRY}
        self.assertEqual("/os-adr:init", by_name["adr-system-present"])
        self.assertEqual("/os-backlog:route", by_name["tracker-configured"])
        self.assertEqual("/os-vault:onboard-project", by_name["project-graph-present"])
        self.assertEqual("/os-status:fix", by_name["config-version-current"])


class JsonRunnerTest(unittest.TestCase):
    def test_shape_has_name_status_message_remediation(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            ctx = make_ctx(project_root=root)
            results = run_all_json(ctx)
            self.assertTrue(results)
            for entry in results:
                self.assertEqual({"name", "status", "message", "remediation"}, set(entry.keys()))

    def test_project_scoped_checks_skipped_when_no_project_root(self):
        results = run_all_json(make_ctx(project_root=None))
        names = {r["name"] for r in results}
        self.assertNotIn("adr-system-present", names)
        self.assertNotIn("tracker-configured", names)

    def test_run_all_json_self_clears_stale_snooze_on_ok(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            (root / ".cc-os" / "status").mkdir(parents=True)
            (root / ".cc-os" / "status" / "snooze-adr-system-present").write_text(
                date.today().isoformat()
            )
            (root / "docs" / "adr").mkdir(parents=True)
            (root / "docs" / "adr" / "README.md").write_text("index")
            ctx = make_ctx(project_root=root)
            run_all_json(ctx)
            self.assertFalse(
                (root / ".cc-os" / "status" / "snooze-adr-system-present").exists()
            )

    def test_subprocess_json_flag_prints_valid_json_array(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            proc = subprocess.run(
                [sys.executable, str(PLUGIN_ROOT / "hooks" / "checks.py"), "--json"],
                cwd=str(root),
                capture_output=True,
                text=True,
            )
            self.assertEqual(0, proc.returncode)
            payload = json.loads(proc.stdout)
            self.assertIsInstance(payload, list)
            names = {e["name"] for e in payload}
            self.assertIn("subagent-model-env-override", names)


class WriteConfigValueTest(unittest.TestCase):
    def test_stamps_new_key(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            write_config_value(root, "version", "1")
            self.assertEqual({"version": "1"}, read_config(root))

    def test_preserves_existing_keys(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "config").write_text("hub = my-hub\ntracker = forgejo:jared/x\n")
            write_config_value(root, "version", "1")
            self.assertEqual(
                {"hub": "my-hub", "tracker": "forgejo:jared/x", "version": "1"},
                read_config(root),
            )

    def test_updates_existing_key_in_place(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "config").write_text("version = 0\nhub = my-hub\n")
            write_config_value(root, "version", "1")
            config = read_config(root)
            self.assertEqual("1", config["version"])
            self.assertEqual("my-hub", config["hub"])

    def test_new_key_written_without_spaces_around_equals(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            write_config_value(root, "version", "1")
            raw = (root / ".cc-os" / "config").read_text()
            self.assertEqual("version=1\n", raw)

    def test_normalizes_existing_spaced_lines_on_rewrite(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "config").write_text("version = 0\nhub = my-hub\n")
            write_config_value(root, "version", "1")
            raw = (root / ".cc-os" / "config").read_text()
            for line in raw.splitlines():
                self.assertNotIn(" = ", line)
                self.assertNotIn(" =", line)
                self.assertNotIn("= ", line)
            self.assertEqual({"version": "1", "hub": "my-hub"}, read_config(root))

    def test_comments_and_blank_lines_untouched(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "config").write_text("# a comment\n\nhub = my-hub\n")
            write_config_value(root, "version", "1")
            raw = (root / ".cc-os" / "config").read_text()
            self.assertIn("# a comment", raw)


class BannerWordingTest(unittest.TestCase):
    def test_banner_header_names_fix_skill(self):
        self.assertIn("/os-status:fix", session_start.BANNER_HEADER)


class RunnerTest(unittest.TestCase):
    """run() contract: routing, aggregation, snooze/suppress, isolation."""

    def run_with(self, registry, ctx, state=None, today=TODAY):
        out = io.StringIO()
        with mock.patch.object(session_start, "REGISTRY", registry):
            session_start.run(ctx, state, today, out)
        raw = out.getvalue()
        return json.loads(raw) if raw else {}

    def check(self, name, result, project_scoped=False):
        return Check(name, lambda ctx: result, project_scoped)

    def test_all_ok_emits_nothing(self):
        registry = [self.check("a", CheckResult("ok")), self.check("b", CheckResult("ok"))]
        self.assertEqual({}, self.run_with(registry, make_ctx()))

    def test_note_passes_through_as_additional_context_without_banner(self):
        registry = [self.check("a", CheckResult("note", "hello"))]
        payload = self.run_with(registry, make_ctx())
        self.assertEqual("hello", payload["hookSpecificOutput"]["additionalContext"])
        self.assertNotIn("systemMessage", payload)

    def test_multiple_warns_aggregate_into_one_banner(self):
        registry = [
            self.check("a", CheckResult("warn", "first problem")),
            self.check("b", CheckResult("warn", "second problem")),
        ]
        payload = self.run_with(registry, make_ctx())
        banner = payload["systemMessage"]
        self.assertIn("first problem", banner)
        self.assertIn("second problem", banner)
        self.assertEqual(1, banner.count(session_start.BANNER_HEADER))

    def test_raising_check_becomes_generic_warn_and_others_still_run(self):
        def boom(ctx):
            raise RuntimeError("kaboom")

        registry = [
            Check("broken", boom, False),
            self.check("healthy", CheckResult("note", "still here")),
        ]
        payload = self.run_with(registry, make_ctx())
        self.assertIn("broken", payload["systemMessage"])
        self.assertNotIn("kaboom", payload["systemMessage"])
        self.assertIn("still here", payload["hookSpecificOutput"]["additionalContext"])

    def test_non_checkresult_return_becomes_generic_warn(self):
        registry = [Check("weird", lambda ctx: "nope", False)]
        payload = self.run_with(registry, make_ctx())
        self.assertIn("weird", payload["systemMessage"])

    def test_project_scoped_checks_skipped_outside_git_project(self):
        registry = [self.check("proj", CheckResult("warn", "boom"), project_scoped=True)]
        self.assertEqual({}, self.run_with(registry, make_ctx(project_root=None)))

    def test_warn_stamps_snooze_and_second_run_same_day_is_silent(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            state = StateDir(root)
            registry = [self.check("a", CheckResult("warn", "boom"))]
            ctx = make_ctx(project_root=root)
            first = self.run_with(registry, ctx, state)
            self.assertIn("boom", first["systemMessage"])
            self.assertTrue((root / ".cc-os" / "status" / "snooze-a").exists())
            self.assertEqual({}, self.run_with(registry, ctx, state))

    def test_next_day_warns_again(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            state = StateDir(root)
            registry = [self.check("a", CheckResult("warn", "boom"))]
            ctx = make_ctx(project_root=root)
            self.run_with(registry, ctx, state, today=YESTERDAY)
            self.assertIn("boom", self.run_with(registry, ctx, state)["systemMessage"])

    def test_suppress_marker_silences_permanently(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "suppress-a").write_text("")
            state = StateDir(root)
            registry = [self.check("a", CheckResult("warn", "boom"))]
            self.assertEqual({}, self.run_with(registry, make_ctx(project_root=root), state))

    def test_notes_are_never_snoozed_or_suppressed(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "suppress-a").write_text("")
            (root / ".cc-os" / "snooze-a").write_text(TODAY.isoformat())
            state = StateDir(root)
            registry = [self.check("a", CheckResult("note", "always"))]
            payload = self.run_with(registry, make_ctx(project_root=root), state)
            self.assertEqual("always", payload["hookSpecificOutput"]["additionalContext"])

    def test_snoozed_warn_does_not_suppress_other_warns(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "snooze-a").write_text(TODAY.isoformat())
            state = StateDir(root)
            registry = [
                self.check("a", CheckResult("warn", "old news")),
                self.check("b", CheckResult("warn", "new problem")),
            ]
            banner = self.run_with(registry, make_ctx(project_root=root), state)["systemMessage"]
            self.assertNotIn("old news", banner)
            self.assertIn("new problem", banner)

    def test_snooze_self_clears_when_check_flips_to_ok(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            state = StateDir(root)
            warn_registry = [self.check("a", CheckResult("warn", "boom"))]
            ctx = make_ctx(project_root=root)
            self.run_with(warn_registry, ctx, state)
            self.assertTrue((root / ".cc-os" / "status" / "snooze-a").exists())

            ok_registry = [self.check("a", CheckResult("ok"))]
            self.run_with(ok_registry, ctx, state)
            self.assertFalse((root / ".cc-os" / "status" / "snooze-a").exists())

    def test_snooze_self_clears_when_check_flips_to_note(self):
        with TemporaryDirectory() as tmp:
            root = git_project(tmp)
            state = StateDir(root)
            warn_registry = [self.check("a", CheckResult("warn", "boom"))]
            ctx = make_ctx(project_root=root)
            self.run_with(warn_registry, ctx, state)
            self.assertTrue((root / ".cc-os" / "status" / "snooze-a").exists())

            note_registry = [self.check("a", CheckResult("note", "fine now"))]
            self.run_with(note_registry, ctx, state)
            self.assertFalse((root / ".cc-os" / "status" / "snooze-a").exists())


class StateTest(unittest.TestCase):
    def test_read_config_parses_key_value_and_skips_comments(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "config").write_text(
                "# comment\nhub = my-hub\nvault_path=/somewhere\nbadline\n"
            )
            self.assertEqual(
                {"hub": "my-hub", "vault_path": "/somewhere"}, read_config(root)
            )

    def test_read_config_missing_is_empty(self):
        with TemporaryDirectory() as tmp:
            self.assertEqual({}, read_config(Path(tmp)))

    def test_corrupt_snooze_stamp_reads_as_unset(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "snooze-a").write_text("not-a-date")
            self.assertFalse(StateDir(root).snoozed("a", TODAY))

    def test_find_project_root_walks_up(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".git").mkdir()
            nested = root / "a" / "b"
            nested.mkdir(parents=True)
            self.assertEqual(root, find_project_root(nested))

    def test_find_project_root_none_outside_git(self):
        with TemporaryDirectory() as tmp:
            self.assertIsNone(find_project_root(Path(tmp)))

    def test_stamp_writes_to_status_subdir(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            StateDir(root).stamp("a", TODAY)
            self.assertTrue((root / ".cc-os" / "status" / "snooze-a").exists())
            self.assertFalse((root / ".cc-os" / "snooze-a").exists())

    def test_snoozed_falls_back_to_legacy_top_level_path(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "snooze-a").write_text(TODAY.isoformat())
            self.assertTrue(StateDir(root).snoozed("a", TODAY))

    def test_suppressed_falls_back_to_legacy_top_level_path(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "suppress-a").write_text("")
            self.assertTrue(StateDir(root).suppressed("a"))

    def test_current_path_wins_over_legacy_when_both_exist(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os" / "status").mkdir(parents=True)
            (root / ".cc-os" / "status" / "snooze-a").write_text(TODAY.isoformat())
            (root / ".cc-os" / "snooze-a").write_text(YESTERDAY.isoformat())
            self.assertTrue(StateDir(root).snoozed("a", TODAY))

    def test_write_migrates_legacy_files_and_removes_them(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "snooze-b").write_text(YESTERDAY.isoformat())
            (root / ".cc-os" / "suppress-c").write_text("")
            StateDir(root).stamp("a", TODAY)
            # Legacy files migrated up and removed from the old location.
            self.assertTrue((root / ".cc-os" / "status" / "snooze-b").exists())
            self.assertTrue((root / ".cc-os" / "status" / "suppress-c").exists())
            self.assertFalse((root / ".cc-os" / "snooze-b").exists())
            self.assertFalse((root / ".cc-os" / "suppress-c").exists())
            # config, if present, is left alone by migration.
            self.assertFalse((root / ".cc-os" / "status" / "config").exists())

    def test_migration_does_not_touch_config(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "config").write_text("tracker=repo:x\n")
            (root / ".cc-os" / "snooze-a").write_text(YESTERDAY.isoformat())
            StateDir(root).stamp("a", TODAY)
            self.assertTrue((root / ".cc-os" / "config").exists())
            self.assertEqual("tracker=repo:x\n", (root / ".cc-os" / "config").read_text())


class ClearSnoozeTest(unittest.TestCase):
    def test_clears_current_path_snooze(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os" / "status").mkdir(parents=True)
            (root / ".cc-os" / "status" / "snooze-a").write_text(TODAY.isoformat())
            clear_snooze(root, "a")
            self.assertFalse((root / ".cc-os" / "status" / "snooze-a").exists())

    def test_clears_legacy_path_snooze(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os").mkdir()
            (root / ".cc-os" / "snooze-a").write_text(TODAY.isoformat())
            clear_snooze(root, "a")
            self.assertFalse((root / ".cc-os" / "snooze-a").exists())

    def test_missing_snooze_is_a_silent_noop(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            clear_snooze(root, "a")  # must not raise

    def test_does_not_clear_other_checks_snooze(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            (root / ".cc-os" / "status").mkdir(parents=True)
            (root / ".cc-os" / "status" / "snooze-a").write_text(TODAY.isoformat())
            (root / ".cc-os" / "status" / "snooze-b").write_text(TODAY.isoformat())
            clear_snooze(root, "a")
            self.assertFalse((root / ".cc-os" / "status" / "snooze-a").exists())
            self.assertTrue((root / ".cc-os" / "status" / "snooze-b").exists())


class WordingInvariantTest(unittest.TestCase):
    """PRESENT_NOTE / ABSENT_NOTE must stay byte-identical to os-adr's
    hooks/session_start.py, the wording source of record (invariants.md)."""

    def os_adr_module(self):
        source = REPO_PLUGINS / "os-adr" / "hooks" / "session_start.py"
        spec = importlib.util.spec_from_file_location("os_adr_session_start", source)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return module

    def test_notes_byte_identical_to_os_adr(self):
        os_adr = self.os_adr_module()
        self.assertEqual(os_adr.PRESENT_NOTE, PRESENT_NOTE)
        self.assertEqual(os_adr.ABSENT_NOTE, ABSENT_NOTE)


class ExitZeroEnvelopeTest(unittest.TestCase):
    def test_main_returns_zero_even_when_run_raises(self):
        with mock.patch.object(session_start, "run", side_effect=RuntimeError):
            self.assertEqual(0, session_start.main())

    def test_subprocess_in_non_git_cwd_exits_zero_silently(self):
        with TemporaryDirectory() as tmp:
            env = {k: v for k, v in os.environ.items() if k != ENV_VAR}
            env["HOME"] = tmp  # no settings.json, no vault
            proc = subprocess.run(
                [sys.executable, str(PLUGIN_ROOT / "hooks" / "session_start.py")],
                cwd=tmp,
                env=env,
                capture_output=True,
                text=True,
            )
            self.assertEqual(0, proc.returncode)
            self.assertEqual("", proc.stdout)
            self.assertFalse((Path(tmp) / ".cc-os").exists())

    def test_subprocess_with_env_override_warns_but_exits_zero(self):
        with TemporaryDirectory() as tmp:
            env = dict(os.environ)
            env["HOME"] = tmp
            env[ENV_VAR] = "haiku"
            proc = subprocess.run(
                [sys.executable, str(PLUGIN_ROOT / "hooks" / "session_start.py")],
                cwd=tmp,
                env=env,
                capture_output=True,
                text=True,
            )
            self.assertEqual(0, proc.returncode)
            payload = json.loads(proc.stdout)
            self.assertIn(ENV_VAR, payload["systemMessage"])


if __name__ == "__main__":
    unittest.main()
