"""Tests for agentmux.core.rollback — manifest serialization, pruning, and execution."""

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

import pytest
from click.testing import CliRunner

from agentmux.cli.main import cli
from agentmux.core.rollback import (
    RollbackEntry,
    RollbackManifest,
    _MAX_MANIFESTS,
    _ROLLBACK_DIR,
    list_manifests,
    load_latest_manifest,
    save_manifest,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_manifest(
    setup_name: str = "test-setup",
    entries: list[RollbackEntry] | None = None,
    *,
    ts: datetime | None = None,
) -> RollbackManifest:
    return RollbackManifest(
        timestamp=ts or datetime.now(tz=timezone.utc),
        setup_name=setup_name,
        entries=entries or [],
    )


# ---------------------------------------------------------------------------
# Serialisation round-trip
# ---------------------------------------------------------------------------


def test_manifest_to_dict_and_back() -> None:
    entry = RollbackEntry(
        target_path="/tmp/file.txt",
        backup_path="/tmp/file.txt.bak.20250101_000000_000000",
        action="updated",
    )
    manifest = _make_manifest(entries=[entry])
    data = manifest.to_dict()
    restored = RollbackManifest.from_dict(data)

    assert restored.setup_name == manifest.setup_name
    assert len(restored.entries) == 1
    assert restored.entries[0].target_path == "/tmp/file.txt"
    assert restored.entries[0].backup_path == entry.backup_path
    assert restored.entries[0].action == "updated"


def test_manifest_round_trip_without_backup_path() -> None:
    entry = RollbackEntry(target_path="/some/new_file.md", backup_path=None, action="created")
    manifest = _make_manifest(entries=[entry])
    restored = RollbackManifest.from_dict(manifest.to_dict())
    assert restored.entries[0].backup_path is None


def test_manifest_timestamp_preserved() -> None:
    ts = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
    manifest = _make_manifest(ts=ts)
    restored = RollbackManifest.from_dict(manifest.to_dict())
    assert restored.timestamp == ts


# ---------------------------------------------------------------------------
# save_manifest / load_latest_manifest
# ---------------------------------------------------------------------------


def test_save_manifest_creates_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", tmp_path / "rollback")
    manifest = _make_manifest("my-setup")
    path = save_manifest(manifest)
    assert path.exists()
    data = json.loads(path.read_text())
    assert data["setup_name"] == "my-setup"


def test_load_latest_manifest_returns_none_when_empty(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", tmp_path / "rollback")
    assert load_latest_manifest("no-setup") is None


def test_load_latest_manifest_returns_most_recent(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    import time

    rollback_dir = tmp_path / "rollback"
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    m1 = _make_manifest("my-setup", [RollbackEntry("/a.md", None, "created")])
    save_manifest(m1)
    time.sleep(0.01)
    m2 = _make_manifest("my-setup", [RollbackEntry("/b.md", None, "created")])
    save_manifest(m2)

    latest = load_latest_manifest("my-setup")
    assert latest is not None
    assert latest.entries[0].target_path == "/b.md"


def test_list_manifests_returns_empty_when_dir_missing(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", tmp_path / "nonexistent")
    assert list_manifests("anything") == []


# ---------------------------------------------------------------------------
# Pruning
# ---------------------------------------------------------------------------


def test_save_manifest_prunes_old_entries(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    import time

    rollback_dir = tmp_path / "rollback"
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    for _ in range(_MAX_MANIFESTS + 2):
        save_manifest(_make_manifest("prune-test"))
        time.sleep(0.01)

    remaining = list_manifests("prune-test")
    assert len(remaining) == _MAX_MANIFESTS


def test_pruning_does_not_affect_other_setups(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    import time

    rollback_dir = tmp_path / "rollback"
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    for _ in range(_MAX_MANIFESTS + 2):
        save_manifest(_make_manifest("setup-a"))
        time.sleep(0.01)

    save_manifest(_make_manifest("setup-b"))

    assert len(list_manifests("setup-a")) == _MAX_MANIFESTS
    assert len(list_manifests("setup-b")) == 1


# ---------------------------------------------------------------------------
# CLI integration: rollback command
# ---------------------------------------------------------------------------


@pytest.fixture
def runner() -> CliRunner:
    """Local runner fixture — intentionally separate from test_cli.py's fixture."""
    return CliRunner()


@pytest.fixture
def setup_dir(tmp_path: Path) -> Path:
    """Minimal agentmux setup directory with a cursor tool configured.

    Intentionally different from the setup_dir in test_cli.py: this fixture
    omits commands/skills/rules content and uses a simpler structure suited
    to rollback-specific tests.
    """
    (tmp_path / "agents").mkdir()
    (tmp_path / "agents" / "helper.md").write_text(
        "---\nname: Helper\ndescription: A helper\n---\nBe helpful."
    )
    (tmp_path / "commands").mkdir()
    (tmp_path / "skills").mkdir()
    (tmp_path / "rules").mkdir()
    (tmp_path / "instructions.md").write_text("Be concise.")
    (tmp_path / "agentmux.toml").write_text(
        """\
[setup]
name = "test-setup"

[tools]
cursor = { enabled = true, strategy = "symlink" }

[components]
agents = ["agents/*.md"]
commands = ["commands/*.md"]
skills = ["skills/*/SKILL.md"]
rules = ["rules/*.md"]
instructions = "instructions.md"
"""
    )
    return tmp_path


def test_rollback_no_history(
    runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", tmp_path / "rollback")
    result = runner.invoke(cli, ["rollback", "no-such-setup"])
    assert result.exit_code != 0
    assert "No rollback history" in result.output


def test_rollback_deletes_created_files(
    runner: CliRunner,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Files created during sync are deleted on rollback."""
    rollback_dir = tmp_path / "rollback"
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    target = tmp_path / "deployed_file.md"
    target.write_text("deployed content")

    entry = RollbackEntry(
        target_path=str(target),
        backup_path=None,
        action="created",
    )
    manifest = _make_manifest("test-setup", [entry])
    save_manifest(manifest)

    result = runner.invoke(cli, ["rollback", "test-setup", "--yes"])
    assert result.exit_code == 0, result.output
    assert not target.exists()
    assert "deleted" in result.output


def test_rollback_restores_updated_files(
    runner: CliRunner,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Files updated during sync are restored from their backup on rollback."""
    rollback_dir = tmp_path / "rollback"
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    target = tmp_path / "existing.md"
    target.write_text("current content")

    backup_file = tmp_path / "existing.md.bak.20250101_000000_000000"
    backup_file.write_text("original content")

    entry = RollbackEntry(
        target_path=str(target),
        backup_path=str(backup_file),
        action="updated",
    )
    manifest = _make_manifest("test-setup", [entry])
    save_manifest(manifest)

    result = runner.invoke(cli, ["rollback", "test-setup", "--yes"])
    assert result.exit_code == 0, result.output
    assert target.exists()
    assert target.read_text() == "original content"
    assert "restored" in result.output


def test_rollback_skips_missing_backup_gracefully(
    runner: CliRunner,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """When a backup file is gone, rollback warns but does not crash."""
    rollback_dir = tmp_path / "rollback"
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    target = tmp_path / "file.md"
    target.write_text("current")

    entry = RollbackEntry(
        target_path=str(target),
        backup_path=str(tmp_path / "nonexistent.bak"),
        action="updated",
    )
    manifest = _make_manifest("test-setup", [entry])
    save_manifest(manifest)

    result = runner.invoke(cli, ["rollback", "test-setup", "--yes"])
    # Should exit successfully (just warn, not error)
    assert "missing" in result.output.lower() or "skipping" in result.output.lower()


def test_rollback_confirms_before_proceeding(
    runner: CliRunner,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Without --yes, rollback prompts; answering N aborts cleanly."""
    rollback_dir = tmp_path / "rollback"
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    target = tmp_path / "file.md"
    target.write_text("content")

    entry = RollbackEntry(target_path=str(target), backup_path=None, action="created")
    manifest = _make_manifest("test-setup", [entry])
    save_manifest(manifest)

    result = runner.invoke(cli, ["rollback", "test-setup"], input="n\n")
    assert result.exit_code != 0
    assert target.exists()  # file was NOT deleted


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------


def test_from_dict_raises_on_missing_key() -> None:
    """from_dict() raises ValueError for manifests missing required keys."""
    with pytest.raises(ValueError, match="Invalid rollback manifest"):
        RollbackManifest.from_dict({"timestamp": "2025-01-01T00:00:00+00:00"})


def test_from_dict_raises_on_bad_timestamp() -> None:
    """from_dict() raises ValueError when timestamp cannot be parsed."""
    with pytest.raises(ValueError, match="Invalid rollback manifest"):
        RollbackManifest.from_dict(
            {"timestamp": "not-a-date", "setup_name": "s", "entries": []}
        )


def test_load_latest_manifest_raises_on_corrupt_json(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """load_latest_manifest() raises ValueError when the manifest JSON is malformed."""
    rollback_dir = tmp_path / "rollback"
    rollback_dir.mkdir(parents=True)
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    corrupt = rollback_dir / "20250101_000000_000000-my-setup.json"
    corrupt.write_text("{this is not valid json")

    with pytest.raises(ValueError, match="Corrupt manifest"):
        load_latest_manifest("my-setup")


# ---------------------------------------------------------------------------
# list_manifests — hyphenated names
# ---------------------------------------------------------------------------


def test_list_manifests_does_not_match_hyphen_suffix(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """list_manifests('setup') must not return manifests for 'team-setup'."""
    rollback_dir = tmp_path / "rollback"
    rollback_dir.mkdir(parents=True)
    monkeypatch.setattr("agentmux.core.rollback._ROLLBACK_DIR", rollback_dir)

    m_setup = _make_manifest("setup")
    m_team = _make_manifest("team-setup")
    save_manifest(m_setup)
    save_manifest(m_team)

    setup_manifests = list_manifests("setup")
    team_manifests = list_manifests("team-setup")

    assert len(setup_manifests) == 1
    assert len(team_manifests) == 1
    assert setup_manifests[0] != team_manifests[0]
