"""Tests for agentmux.core.registry — Registry and RegistryEntry.

All tests use ``tmp_path`` for filesystem isolation; the real
``~/.agentmux/registry.json`` is never touched.
"""

import json
from datetime import datetime, timezone
from pathlib import Path

import pytest

from agentmux.core.registry import Registry, RegistryEntry

# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------

_FIXED_TS = datetime(2026, 2, 23, 10, 0, 0, tzinfo=timezone.utc)
_FIXED_TS_STR = "2026-02-23T10:00:00.000000Z"
_FIXED_TS_STR_LEGACY = "2026-02-23T10:00:00Z"
_FAKE_SHA = "abc123def456abc123def456abc123def456abc123"
_FAKE_URL = "https://github.com/user/my-setup"


def _make_entry(
    name: str = "my-setup",
    local_path: Path | None = None,
    sha: str = _FAKE_SHA,
    url: str = _FAKE_URL,
    installed_at: datetime = _FIXED_TS,
) -> RegistryEntry:
    return RegistryEntry(
        name=name,
        source_url=url,
        installed_sha=sha,
        installed_at=installed_at,
        local_path=local_path or Path(f"/home/user/.agentmux/setups/{name}"),
    )


def _registry_json(tmp_path: Path, setups: dict[str, object]) -> Path:
    """Write a registry.json and return its path."""
    reg_file = tmp_path / "registry.json"
    reg_file.write_text(json.dumps({"setups": setups}), encoding="utf-8")
    return reg_file


# ---------------------------------------------------------------------------
# RegistryEntry.from_dict
# ---------------------------------------------------------------------------


def test_registry_entry_from_dict_valid(tmp_path: Path) -> None:
    """from_dict correctly deserialises a well-formed entry dict."""
    data = {
        "source_url": _FAKE_URL,
        "installed_sha": _FAKE_SHA,
        "installed_at": _FIXED_TS_STR,
        "local_path": "/home/user/.agentmux/setups/my-setup",
    }
    entry = RegistryEntry.from_dict("my-setup", data)

    assert entry.name == "my-setup"
    assert entry.source_url == _FAKE_URL
    assert entry.installed_sha == _FAKE_SHA
    assert entry.installed_at == _FIXED_TS
    assert entry.local_path == Path("/home/user/.agentmux/setups/my-setup")


def test_registry_entry_from_dict_missing_key_raises(tmp_path: Path) -> None:
    """from_dict raises ValueError when a required key is absent."""
    incomplete = {
        "source_url": _FAKE_URL,
        "installed_sha": _FAKE_SHA,
        # "installed_at" is missing
        "local_path": "/some/path",
    }
    with pytest.raises(ValueError, match="missing required keys.*installed_at"):
        RegistryEntry.from_dict("bad-entry", incomplete)


def test_registry_entry_from_dict_bad_timestamp_raises(tmp_path: Path) -> None:
    """from_dict raises ValueError for a non-ISO-8601 installed_at value."""
    data = {
        "source_url": _FAKE_URL,
        "installed_sha": _FAKE_SHA,
        "installed_at": "not-a-timestamp",
        "local_path": "/some/path",
    }
    with pytest.raises(ValueError, match="invalid 'installed_at' timestamp"):
        RegistryEntry.from_dict("bad-ts", data)


def test_registry_entry_from_dict_legacy_timestamp_accepted(tmp_path: Path) -> None:
    """from_dict accepts the legacy ISO-8601 format (no microseconds) for backwards compatibility."""
    data = {
        "source_url": _FAKE_URL,
        "installed_sha": _FAKE_SHA,
        "installed_at": _FIXED_TS_STR_LEGACY,
        "local_path": "/some/path",
    }
    entry = RegistryEntry.from_dict("legacy", data)
    assert entry.installed_at == _FIXED_TS


# ---------------------------------------------------------------------------
# RegistryEntry.to_dict
# ---------------------------------------------------------------------------


def test_registry_entry_to_dict_round_trip(tmp_path: Path) -> None:
    """to_dict produces the dict that from_dict can parse back losslessly."""
    original = _make_entry()
    serialised = original.to_dict()
    restored = RegistryEntry.from_dict(original.name, serialised)

    assert restored.name == original.name
    assert restored.source_url == original.source_url
    assert restored.installed_sha == original.installed_sha
    assert restored.installed_at == original.installed_at
    assert restored.local_path == original.local_path


def test_registry_entry_to_dict_timestamp_format(tmp_path: Path) -> None:
    """to_dict serialises installed_at in the expected ISO-8601 UTC format."""
    entry = _make_entry()
    assert entry.to_dict()["installed_at"] == _FIXED_TS_STR


# ---------------------------------------------------------------------------
# Registry.load — missing file
# ---------------------------------------------------------------------------


def test_registry_load_missing_file_returns_empty(tmp_path: Path) -> None:
    """load() returns an empty Registry when registry.json does not exist."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)

    assert registry.list_all() == []


# ---------------------------------------------------------------------------
# Registry.load — valid file
# ---------------------------------------------------------------------------


def test_registry_load_reads_entries(tmp_path: Path) -> None:
    """load() deserialises all entries from a well-formed registry.json."""
    reg_file = _registry_json(
        tmp_path,
        {
            "my-setup": {
                "source_url": _FAKE_URL,
                "installed_sha": _FAKE_SHA,
                "installed_at": _FIXED_TS_STR,
                "local_path": "/home/user/.agentmux/setups/my-setup",
            }
        },
    )
    registry = Registry.load(registry_file=reg_file)

    assert len(registry.list_all()) == 1
    entry = registry.get("my-setup")
    assert entry is not None
    assert entry.source_url == _FAKE_URL


def test_registry_load_multiple_entries(tmp_path: Path) -> None:
    """load() handles files with multiple setups correctly."""
    reg_file = _registry_json(
        tmp_path,
        {
            "alpha": {
                "source_url": "https://github.com/a/alpha",
                "installed_sha": "aaa",
                "installed_at": _FIXED_TS_STR,
                "local_path": "/a",
            },
            "beta": {
                "source_url": "https://github.com/b/beta",
                "installed_sha": "bbb",
                "installed_at": _FIXED_TS_STR,
                "local_path": "/b",
            },
        },
    )
    registry = Registry.load(registry_file=reg_file)

    assert len(registry.list_all()) == 2
    assert registry.get("alpha") is not None
    assert registry.get("beta") is not None


# ---------------------------------------------------------------------------
# Registry.load — error cases
# ---------------------------------------------------------------------------


def test_registry_load_invalid_json_raises(tmp_path: Path) -> None:
    """load() raises ValueError for malformed JSON."""
    reg_file = tmp_path / "registry.json"
    reg_file.write_text("{not valid json}", encoding="utf-8")

    with pytest.raises(ValueError, match="Failed to parse registry file"):
        Registry.load(registry_file=reg_file)


def test_registry_load_missing_setups_key_raises(tmp_path: Path) -> None:
    """load() raises ValueError when 'setups' key is absent from the JSON."""
    reg_file = tmp_path / "registry.json"
    reg_file.write_text('{"other_key": {}}', encoding="utf-8")

    with pytest.raises(ValueError, match="must be a JSON object with a 'setups' key"):
        Registry.load(registry_file=reg_file)


def test_registry_load_invalid_entry_dict_raises(tmp_path: Path) -> None:
    """load() raises ValueError when a setup entry is not a JSON object."""
    reg_file = _registry_json(tmp_path, {"bad-entry": "not-a-dict"})

    with pytest.raises(ValueError, match="must be a JSON object"):
        Registry.load(registry_file=reg_file)


def test_registry_load_entry_with_missing_key_raises(tmp_path: Path) -> None:
    """load() raises ValueError when a setup entry is missing required keys."""
    reg_file = _registry_json(
        tmp_path,
        {
            "broken": {
                "source_url": _FAKE_URL,
                # missing installed_sha, installed_at, local_path
            }
        },
    )

    with pytest.raises(ValueError, match="Failed to load registry entry 'broken'"):
        Registry.load(registry_file=reg_file)


# ---------------------------------------------------------------------------
# Registry.save
# ---------------------------------------------------------------------------


def test_registry_save_creates_file(tmp_path: Path) -> None:
    """save() writes a valid registry.json to disk."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)
    registry._entries["my-setup"] = _make_entry(local_path=tmp_path / "my-setup")
    registry.save()

    assert reg_file.exists()
    payload = json.loads(reg_file.read_text(encoding="utf-8"))
    assert "setups" in payload
    assert "my-setup" in payload["setups"]


def test_registry_save_creates_parent_directory(tmp_path: Path) -> None:
    """save() creates the parent directory (~/.agentmux/) if it is absent."""
    nested = tmp_path / "deep" / "nested"
    reg_file = nested / "registry.json"
    registry = Registry(_registry_file=reg_file)
    registry.save()

    assert nested.is_dir()
    assert reg_file.exists()


def test_registry_save_is_valid_json(tmp_path: Path) -> None:
    """save() produces JSON that round-trips without data loss."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)
    entry = _make_entry(local_path=tmp_path / "my-setup")
    registry._entries["my-setup"] = entry
    registry.save()

    reloaded = Registry.load(registry_file=reg_file)
    restored_entry = reloaded.get("my-setup")
    assert restored_entry is not None
    assert restored_entry.installed_sha == _FAKE_SHA


# ---------------------------------------------------------------------------
# Registry.add
# ---------------------------------------------------------------------------


def test_registry_add_persists_entry(tmp_path: Path) -> None:
    """add() stores the entry in memory and writes it to disk immediately."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)
    entry = _make_entry(local_path=tmp_path / "my-setup")

    registry.add(entry)

    assert reg_file.exists()
    reloaded = Registry.load(registry_file=reg_file)
    assert reloaded.get("my-setup") is not None


def test_registry_add_overwrites_existing_entry(tmp_path: Path) -> None:
    """add() replaces an entry with the same name without error."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)

    old_entry = _make_entry(sha="old_sha")
    registry.add(old_entry)

    new_entry = _make_entry(sha="new_sha")
    registry.add(new_entry)

    assert len(registry.list_all()) == 1
    assert registry.get("my-setup") is not None
    assert registry.get("my-setup").installed_sha == "new_sha"  # type: ignore[union-attr]


# ---------------------------------------------------------------------------
# Registry.remove
# ---------------------------------------------------------------------------


def test_registry_remove_deletes_entry(tmp_path: Path) -> None:
    """remove() removes the entry from memory and persists the change."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)
    registry.add(_make_entry())

    registry.remove("my-setup")

    assert registry.get("my-setup") is None
    reloaded = Registry.load(registry_file=reg_file)
    assert reloaded.get("my-setup") is None


def test_registry_remove_unknown_name_raises(tmp_path: Path) -> None:
    """remove() raises KeyError for a name that does not exist."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)

    with pytest.raises(KeyError, match="'ghost-setup'"):
        registry.remove("ghost-setup")


# ---------------------------------------------------------------------------
# Registry.get
# ---------------------------------------------------------------------------


def test_registry_get_returns_none_for_unknown(tmp_path: Path) -> None:
    """get() returns None when the name is not in the registry."""
    registry = Registry.load(registry_file=tmp_path / "registry.json")

    assert registry.get("no-such-setup") is None


def test_registry_get_returns_correct_entry(tmp_path: Path) -> None:
    """get() returns the entry matching the requested name."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)
    registry.add(_make_entry(name="target", sha="xyz"))
    registry.add(_make_entry(name="other", sha="abc"))

    entry = registry.get("target")
    assert entry is not None
    assert entry.installed_sha == "xyz"


# ---------------------------------------------------------------------------
# Registry.list_all
# ---------------------------------------------------------------------------


def test_registry_list_all_empty(tmp_path: Path) -> None:
    """list_all() returns an empty list when the registry has no entries."""
    registry = Registry.load(registry_file=tmp_path / "registry.json")

    assert registry.list_all() == []


def test_registry_list_all_sorted_by_name(tmp_path: Path) -> None:
    """list_all() returns entries sorted alphabetically by name."""
    reg_file = tmp_path / "registry.json"
    registry = Registry.load(registry_file=reg_file)
    registry.add(_make_entry(name="zebra"))
    registry.add(_make_entry(name="alpha"))
    registry.add(_make_entry(name="middle"))

    names = [e.name for e in registry.list_all()]
    assert names == ["alpha", "middle", "zebra"]
