"""Tests for Phase 3 safety enhancements: context-aware snapshots, backups, atomic writes, retry."""

from __future__ import annotations

import time
from pathlib import Path
from unittest.mock import patch

from ilum.core.retry import with_retry
from ilum.core.safety import (
    ValuesSnapshot,
    _sanitize_context,
    delete_snapshot,
    list_backups,
    load_backup,
    load_snapshot,
    save_backup,
    save_snapshot,
)
from ilum.errors import ClusterConnectionError, ModuleError


class TestSanitizeContext:
    def test_simple_name(self) -> None:
        assert _sanitize_context("minikube") == "minikube"

    def test_slashes_replaced(self) -> None:
        assert (
            _sanitize_context("arn:aws:eks:us-east-1:123/cluster")
            == "arn_aws_eks_us-east-1_123_cluster"
        )

    def test_dots_replaced(self) -> None:
        assert _sanitize_context("gke.project.zone") == "gke_project_zone"


class TestContextAwareSnapshots:
    def test_snapshot_path_includes_context(self, tmp_path: Path) -> None:
        snap = ValuesSnapshot(
            release="ilum",
            namespace="default",
            timestamp="",
            chart_version="6.7.0",
            values={"a": 1},
            operation="install",
        )
        save_snapshot(snap, tmp_path, context="minikube")

        # Should load with same context
        loaded = load_snapshot("ilum", "default", tmp_path, context="minikube")
        assert loaded is not None
        assert loaded.values == {"a": 1}

    def test_different_contexts_separate(self, tmp_path: Path) -> None:
        snap1 = ValuesSnapshot(
            release="ilum",
            namespace="default",
            timestamp="",
            chart_version="6.7.0",
            values={"ctx": "one"},
            operation="install",
        )
        snap2 = ValuesSnapshot(
            release="ilum",
            namespace="default",
            timestamp="",
            chart_version="6.7.0",
            values={"ctx": "two"},
            operation="install",
        )
        save_snapshot(snap1, tmp_path, context="ctx1")
        save_snapshot(snap2, tmp_path, context="ctx2")

        loaded1 = load_snapshot("ilum", "default", tmp_path, context="ctx1")
        loaded2 = load_snapshot("ilum", "default", tmp_path, context="ctx2")
        assert loaded1 is not None and loaded1.values == {"ctx": "one"}
        assert loaded2 is not None and loaded2.values == {"ctx": "two"}

    def test_legacy_path_migration(self, tmp_path: Path) -> None:
        """If new path doesn't exist but legacy does, copy it."""
        # Create a legacy snapshot manually
        from ruamel.yaml import YAML

        yaml = YAML()
        legacy_dir = tmp_path / "snapshots"
        legacy_dir.mkdir(parents=True)
        legacy_path = legacy_dir / "ilum--default.yaml"
        yaml.dump(
            {
                "release": "ilum",
                "namespace": "default",
                "timestamp": "2024-01-01",
                "chart_version": "6.7.0",
                "values": {"legacy": True},
                "operation": "install",
            },
            legacy_path,
        )

        # load_snapshot with context should find and migrate
        with patch("ilum.core.safety._get_current_context", return_value="minikube"):
            loaded = load_snapshot("ilum", "default", tmp_path, context="minikube")

        assert loaded is not None
        assert loaded.values == {"legacy": True}

    def test_delete_with_context(self, tmp_path: Path) -> None:
        snap = ValuesSnapshot(
            release="ilum",
            namespace="default",
            timestamp="",
            chart_version="",
            values={"x": 1},
            operation="install",
        )
        save_snapshot(snap, tmp_path, context="ctx")
        assert load_snapshot("ilum", "default", tmp_path, context="ctx") is not None

        delete_snapshot("ilum", "default", tmp_path, context="ctx")
        assert load_snapshot("ilum", "default", tmp_path, context="ctx") is None


class TestBackups:
    def test_save_and_list_backups(self, tmp_path: Path) -> None:
        save_backup("ilum", "default", {"v": 1}, tmp_path, context="ctx")
        backups = list_backups("ilum", "default", tmp_path, context="ctx")
        assert len(backups) == 1

    def test_backups_capped_at_10(self, tmp_path: Path) -> None:
        for i in range(15):
            save_backup("ilum", "default", {"v": i}, tmp_path, context="ctx")
            time.sleep(0.01)  # ensure unique timestamps

        backups = list_backups("ilum", "default", tmp_path, context="ctx")
        assert len(backups) <= 10

    def test_backups_sorted_newest_first(self, tmp_path: Path) -> None:
        save_backup("ilum", "default", {"v": 1}, tmp_path, context="ctx")
        time.sleep(1.1)  # timestamp uses seconds granularity
        save_backup("ilum", "default", {"v": 2}, tmp_path, context="ctx")

        backups = list_backups("ilum", "default", tmp_path, context="ctx")
        assert len(backups) == 2
        # First should be newer (reverse sorted)
        assert backups[0].name > backups[1].name

    def test_load_backup(self, tmp_path: Path) -> None:
        save_backup("ilum", "default", {"key": "value"}, tmp_path, context="ctx")
        backups = list_backups("ilum", "default", tmp_path, context="ctx")
        assert len(backups) == 1
        loaded = load_backup(backups[0])
        assert loaded == {"key": "value"}

    def test_list_backups_empty_dir(self, tmp_path: Path) -> None:
        backups = list_backups("ilum", "default", tmp_path, context="ctx")
        assert backups == []


class TestRetry:
    def test_succeeds_first_try(self) -> None:
        def ok() -> str:
            return "success"

        result = with_retry(ok, max_attempts=3, backoff=0.01)
        assert result == "success"

    def test_retries_on_retryable_error(self) -> None:
        call_count = 0

        def flaky() -> str:
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise ClusterConnectionError("transient")
            return "recovered"

        result = with_retry(flaky, max_attempts=3, backoff=0.01)
        assert result == "recovered"
        assert call_count == 3

    def test_does_not_retry_non_retryable(self) -> None:
        def bad() -> str:
            raise ModuleError("permanent")

        import pytest

        with pytest.raises(ModuleError):
            with_retry(bad, max_attempts=3, backoff=0.01)

    def test_raises_after_max_attempts(self) -> None:
        def always_fail() -> str:
            raise ClusterConnectionError("down")

        import pytest

        with pytest.raises(ClusterConnectionError, match="down"):
            with_retry(always_fail, max_attempts=2, backoff=0.01)

    def test_respects_max_attempts(self) -> None:
        call_count = 0

        def counting() -> str:
            nonlocal call_count
            call_count += 1
            raise ClusterConnectionError("fail")

        import pytest

        with pytest.raises(ClusterConnectionError):
            with_retry(counting, max_attempts=3, backoff=0.01)
        assert call_count == 3


class TestAtomicConfigWrite:
    def test_atomic_write_creates_file(self, tmp_path: Path, monkeypatch) -> None:
        """Config save should produce a valid file even on first write."""
        import sys

        if sys.platform == "win32":
            monkeypatch.setenv("APPDATA", str(tmp_path / "appdata"))
            monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "localappdata"))
        else:
            monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
            monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
            monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "state"))
            monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "cache"))

        from ilum.config.manager import ConfigManager
        from ilum.config.models import IlumConfig
        from ilum.config.paths import IlumPaths

        paths = IlumPaths.default()
        paths.ensure_dirs()
        mgr = ConfigManager(paths)
        config = IlumConfig(active_profile="test")
        mgr.save(config)

        loaded = mgr.load()
        assert loaded.active_profile == "test"
