"""``settings.json`` merge, idempotence, and refusal — the destructive-edit guards.

``settings.json`` is the user's file, shared with every other hook, permission, and
setting they have configured. Two properties this file exists to hold:

* **Merge, never clobber** — everything we did not write survives, byte for byte in
  meaning, through install *and* uninstall.
* **Refuse rather than guess** — on JSON we cannot parse, nothing is written at all.
  Silently rewriting a file whose contents we could not understand is how a tool
  destroys hand-tuned configuration.

Adapted with attribution from the Sooth kit's ``shp/adopt/`` + ``tests/test_adopt.py``.
Copy, not a dependency.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any

import pytest

from mgf.fed.sessionhooks import _install

if TYPE_CHECKING:
    from pathlib import Path


@pytest.fixture
def repo(tmp_path: Path) -> Path:
    root = tmp_path / "repo"
    (root / ".git").mkdir(parents=True)
    return root


def _settings(repo: Path) -> dict[str, Any]:
    return json.loads(_install.settings_path(repo).read_text(encoding="utf-8"))


def _write_settings(repo: Path, document: Any) -> None:
    path = _install.settings_path(repo)
    path.parent.mkdir(parents=True, exist_ok=True)
    text = document if isinstance(document, str) else json.dumps(document, indent=2)
    path.write_text(text, encoding="utf-8")


class TestTheShim:
    def test_it_reaches_the_installed_entrypoint_and_copies_nothing(self, repo: Path) -> None:
        # SHP-09, one implementation: adoption writes a command, not a kit. Nothing
        # is copied into the repo, so there is no second copy to drift.
        changed, _ = _install.install(repo)
        assert changed is True
        assert _install.HOOK_ENTRYPOINT in _install.HOOK_COMMAND
        assert sorted(p.name for p in (repo / ".claude").iterdir()) == ["settings.json"]

    def test_it_is_fail_open_by_construction(self) -> None:
        # `command -v` means a repo without mgf-fed starts normally; `; exit 0` means
        # even a crashing entrypoint cannot make a session unstartable.
        assert _install.HOOK_COMMAND.startswith("command -v mgf-fed >/dev/null 2>&1 &&")
        assert _install.HOOK_COMMAND.endswith("; exit 0")

    def test_only_session_start_is_wired(self, repo: Path) -> None:
        _install.install(repo)
        assert list(_settings(repo)["hooks"]) == ["SessionStart"]


class TestIdempotence:
    def test_installing_twice_writes_once(self, repo: Path) -> None:
        first_changed, _ = _install.install(repo)
        after_first = _settings(repo)
        second_changed, message = _install.install(repo)
        assert (first_changed, second_changed) == (True, False)
        assert "already installed" in message
        assert _settings(repo) == after_first

    def test_ten_installs_leave_exactly_one_entry(self, repo: Path) -> None:
        for _ in range(10):
            _install.install(repo)
        assert _install.status(repo)["entries"] == 1

    def test_a_drifted_shim_is_replaced_not_duplicated(self, repo: Path) -> None:
        # An older shim (or a hand-edited timeout) still reaches our entrypoint, so
        # it is OURS: upgrade it in place. Appending a second would double the hook.
        _write_settings(
            repo,
            {
                "hooks": {
                    "SessionStart": [
                        {
                            "hooks": [
                                {
                                    "type": "command",
                                    "command": f"{_install.HOOK_ENTRYPOINT} --old-flag",
                                    "timeout": 99,
                                }
                            ]
                        }
                    ]
                }
            },
        )
        changed, message = _install.install(repo)
        assert changed is True
        assert "updated" in message
        status = _install.status(repo)
        assert status["entries"] == 1
        assert status["current"] is True
        # A hand-set timeout is the user's; we do not overwrite it.
        entry = _settings(repo)["hooks"]["SessionStart"][0]["hooks"][0]
        assert entry["timeout"] == 99

    def test_uninstall_then_install_round_trips(self, repo: Path) -> None:
        _write_settings(repo, {"permissions": {"allow": ["Bash(ls:*)"]}})
        before = _settings(repo)
        _install.install(repo)
        _install.uninstall(repo)
        assert _settings(repo) == before


class TestMergeNeverClobbers:
    def test_foreign_settings_survive(self, repo: Path) -> None:
        original = {
            "permissions": {"allow": ["Bash(ls:*)"], "deny": ["Bash(rm:*)"]},
            "env": {"FOO": "bar"},
            "model": "opus",
            "hooks": {
                "SessionStart": [{"hooks": [{"type": "command", "command": "echo hi"}]}],
                "Stop": [{"hooks": [{"type": "command", "command": "echo bye"}]}],
                "PostToolUse": [
                    {"matcher": "Edit", "hooks": [{"type": "command", "command": "echo edit"}]}
                ],
            },
        }
        _write_settings(repo, original)
        _install.install(repo)
        merged = _settings(repo)
        for key in ("permissions", "env", "model"):
            assert merged[key] == original[key]
        assert merged["hooks"]["Stop"] == original["hooks"]["Stop"]
        assert merged["hooks"]["PostToolUse"] == original["hooks"]["PostToolUse"]
        # Their SessionStart hook is untouched; ours is appended alongside.
        assert merged["hooks"]["SessionStart"][0] == original["hooks"]["SessionStart"][0]
        assert len(merged["hooks"]["SessionStart"]) == 2

    def test_uninstall_removes_only_ours(self, repo: Path) -> None:
        original = {
            "hooks": {
                "SessionStart": [{"hooks": [{"type": "command", "command": "echo hi"}]}],
                "Stop": [{"hooks": [{"type": "command", "command": "echo bye"}]}],
            }
        }
        _write_settings(repo, original)
        _install.install(repo)
        changed, _ = _install.uninstall(repo)
        assert changed is True
        assert _settings(repo) == original

    def test_uninstall_prunes_only_containers_it_emptied(self, repo: Path) -> None:
        _install.install(repo)
        _install.uninstall(repo)
        # No `"hooks": {}` residue, and no residual SessionStart key.
        assert "hooks" not in _settings(repo)

    def test_a_shared_group_keeps_the_other_entry(self, repo: Path) -> None:
        _write_settings(
            repo,
            {
                "hooks": {
                    "SessionStart": [
                        {
                            "hooks": [
                                {"type": "command", "command": "echo hi"},
                                {"type": "command", "command": _install.HOOK_COMMAND},
                            ]
                        }
                    ]
                }
            },
        )
        _install.uninstall(repo)
        entries = _settings(repo)["hooks"]["SessionStart"][0]["hooks"]
        assert entries == [{"type": "command", "command": "echo hi"}]

    def test_uninstall_when_absent_changes_nothing(self, repo: Path) -> None:
        _write_settings(repo, {"model": "opus"})
        changed, message = _install.uninstall(repo)
        assert changed is False
        assert "not installed" in message
        assert _settings(repo) == {"model": "opus"}

    def test_an_empty_file_is_treated_as_an_empty_document(self, repo: Path) -> None:
        _write_settings(repo, "")
        changed, _ = _install.install(repo)
        assert changed is True
        assert _install.status(repo)["installed"] is True


class TestRefusal:
    def test_unparseable_json_refuses_and_writes_nothing(self, repo: Path) -> None:
        broken = '{ "permissions": [oops,\n'
        _write_settings(repo, broken)
        with pytest.raises(_install.SettingsRefused) as caught:
            _install.install(repo)
        message = str(caught.value)
        assert "not valid JSON" in message
        assert "line 1" in message  # names WHERE, so it is fixable by hand
        assert "Nothing was written" in message
        assert _install.settings_path(repo).read_text(encoding="utf-8") == broken

    def test_uninstall_refuses_the_same_way(self, repo: Path) -> None:
        _write_settings(repo, "{ nope")
        with pytest.raises(_install.SettingsRefused):
            _install.uninstall(repo)

    @pytest.mark.parametrize("document", ["[1, 2, 3]", '"a string"', "42", "null"])
    def test_a_non_object_document_is_refused(self, repo: Path, document: str) -> None:
        _write_settings(repo, document)
        with pytest.raises(_install.SettingsRefused) as caught:
            _install.install(repo)
        assert "not a JSON object" in str(caught.value)

    def test_a_non_object_hooks_key_is_refused(self, repo: Path) -> None:
        _write_settings(repo, {"hooks": ["not", "an", "object"]})
        with pytest.raises(_install.SettingsRefused) as caught:
            _install.install(repo)
        assert "`hooks` key holds a list" in str(caught.value)

    def test_a_non_list_session_start_is_refused(self, repo: Path) -> None:
        _write_settings(repo, {"hooks": {"SessionStart": {"not": "a list"}}})
        with pytest.raises(_install.SettingsRefused) as caught:
            _install.install(repo)
        assert "SessionStart" in str(caught.value)
        assert "not a list" in str(caught.value)

    def test_malformed_groups_are_tolerated_not_refused(self, repo: Path) -> None:
        # A stray non-object in the list is someone else's business. We add ours and
        # leave theirs exactly where it was.
        _write_settings(repo, {"hooks": {"SessionStart": ["a stray string", 42]}})
        changed, _ = _install.install(repo)
        assert changed is True
        groups = _settings(repo)["hooks"]["SessionStart"]
        assert groups[0] == "a stray string"
        assert groups[1] == 42


class TestStatus:
    def test_fresh_repo(self, repo: Path) -> None:
        report = _install.status(repo)
        assert report["installed"] is False
        assert report["settings_exists"] is False
        assert report["problem"] is None

    def test_installed_and_current(self, repo: Path) -> None:
        _install.install(repo)
        report = _install.status(repo)
        assert (report["installed"], report["current"], report["entries"]) == (True, True, 1)

    def test_stale_is_reported_not_silently_fixed(self, repo: Path) -> None:
        _write_settings(
            repo,
            {
                "hooks": {
                    "SessionStart": [
                        {
                            "hooks": [
                                {"type": "command", "command": f"{_install.HOOK_ENTRYPOINT} --old"}
                            ]
                        }
                    ]
                }
            },
        )
        report = _install.status(repo)
        assert report["installed"] is True
        assert report["current"] is False

    def test_status_reports_a_broken_file_instead_of_raising(self, repo: Path) -> None:
        # A status command that cannot answer "is it on?" is worse than useless.
        _write_settings(repo, "{ broken")
        report = _install.status(repo)
        assert report["problem"] is not None
        assert report["installed"] is False


def test_written_json_is_readable_by_hand(repo: Path) -> None:
    # settings.json is a file humans edit. Two-space indent, trailing newline.
    _install.install(repo)
    text = _install.settings_path(repo).read_text(encoding="utf-8")
    assert text.endswith("}\n")
    assert '\n  "hooks"' in text
