# platform/tests/test_pipeline_phases.py
# ═══════════════════════════════════════════════════════════════
# Unit tests for Phase 1 (Backup) and Phase 2 (Setup) engines.
# Validates snapshot creation, integrity, list/prune/restore,
# YAML parsing, env-var resolution, adapter validation, and
# session_config.json emission.
# ═══════════════════════════════════════════════════════════════

from __future__ import annotations

import json
import os
import tarfile
import textwrap
from datetime import datetime, timezone
from pathlib import Path

import pytest

from deskclaw.backup.engine import (
    BackupEngine,
    BackupError,
    IntegrityError,
    SnapshotManifest,
    verify_integrity,
)
from deskclaw.setup.loader import (
    AdapterValidationError,
    EnvVarMissingError,
    PluginConfig,
    SetupError,
    SetupLoader,
)

# ═══════════════════════════════════════════════════════════════
# Phase 1: Backup Engine Tests
# ═══════════════════════════════════════════════════════════════


class TestSnapshotManifest:
    def test_roundtrip(self):
        m = SnapshotManifest(
            snapshot_id="20260311T120000_test",
            agent="test",
            timestamp="2026-03-11T12:00:00+00:00",
            paths_backed_up=["/tmp/a"],
            sha256="abc123",
            size_bytes=1024,
        )
        d = m.to_dict()
        m2 = SnapshotManifest.from_dict(d)
        assert m == m2
        assert d["snapshot_id"] == "20260311T120000_test"
        assert d["deskclaw_version"] == "0.5.0"


class TestBackupCreate:
    def test_create_snapshot(self, tmp_path: Path):
        """Create a snapshot from a temp directory and verify manifest + archive."""
        # Setup source files
        src = tmp_path / "agent_config"
        src.mkdir()
        (src / "config.yaml").write_text("key: value")
        (src / "data.txt").write_text("hello world")

        snap_dir = tmp_path / "snapshots"
        engine = BackupEngine(snapshot_dir=snap_dir)

        ts = datetime(2026, 3, 11, 18, 35, 0, tzinfo=timezone.utc)
        manifest = engine.create("test-agent", [str(src)], timestamp=ts)

        assert manifest.agent == "test-agent"
        assert manifest.snapshot_id == "20260311T183500_test-agent"
        assert manifest.size_bytes > 0
        assert len(manifest.sha256) == 64  # hex SHA256

        # Archive file exists
        archive = snap_dir / "20260311T183500_test-agent.tar.gz"
        assert archive.exists()

        # Manifest JSON exists
        manifest_file = snap_dir / "20260311T183500_test-agent_manifest.json"
        assert manifest_file.exists()
        data = json.loads(manifest_file.read_text())
        assert data["agent"] == "test-agent"

    def test_create_excludes_patterns(self, tmp_path: Path):
        """Verify that excluded patterns (.git, __pycache__) are not archived."""
        src = tmp_path / "project"
        src.mkdir()
        (src / "main.py").write_text("print('hi')")
        git_dir = src / ".git"
        git_dir.mkdir()
        (git_dir / "HEAD").write_text("ref: refs/heads/main")
        pycache = src / "__pycache__"
        pycache.mkdir()
        (pycache / "main.cpython-311.pyc").write_bytes(b"\x00")

        snap_dir = tmp_path / "snapshots"
        engine = BackupEngine(snapshot_dir=snap_dir)
        manifest = engine.create("excl-agent", [str(src)])

        # Inspect archive contents
        archive = snap_dir / f"{manifest.snapshot_id}.tar.gz"
        with tarfile.open(archive, "r:gz") as tar:
            names = tar.getnames()
        # .git and __pycache__ should be excluded
        assert not any(".git" in n for n in names)
        assert not any("__pycache__" in n for n in names)
        # main.py should be present
        assert any("main.py" in n for n in names)

    def test_create_no_valid_paths_raises(self, tmp_path: Path):
        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        with pytest.raises(BackupError, match="No valid backup paths"):
            engine.create("agent", ["/nonexistent/path/xyz"])


class TestBackupList:
    def test_list_empty(self, tmp_path: Path):
        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        assert engine.list_snapshots() == []

    def test_list_and_filter(self, tmp_path: Path):
        src = tmp_path / "src"
        src.mkdir()
        (src / "f.txt").write_text("x")

        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        ts1 = datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
        ts2 = datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc)
        engine.create("alpha", [str(src)], timestamp=ts1)
        engine.create("beta", [str(src)], timestamp=ts2)

        all_snaps = engine.list_snapshots()
        assert len(all_snaps) == 2

        alpha_only = engine.list_snapshots(agent="alpha")
        assert len(alpha_only) == 1
        assert alpha_only[0].agent == "alpha"


class TestBackupPrune:
    def test_prune_keeps_n(self, tmp_path: Path):
        src = tmp_path / "src"
        src.mkdir()
        (src / "f.txt").write_text("x")

        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        for i in range(5):
            ts = datetime(2026, 1, i + 1, 0, 0, 0, tzinfo=timezone.utc)
            engine.create("agent", [str(src)], timestamp=ts)

        assert len(engine.list_snapshots()) == 5
        pruned = engine.prune(keep=2)
        assert len(pruned) == 3
        remaining = engine.list_snapshots()
        assert len(remaining) == 2


class TestBackupRestore:
    def test_restore_roundtrip(self, tmp_path: Path):
        """Backup, destroy source, restore, verify contents."""
        src = tmp_path / "myconfig"
        src.mkdir()
        (src / "settings.yaml").write_text("mode: test")

        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        manifest = engine.create("restore-agent", [str(src)])

        # Destroy the source
        (src / "settings.yaml").unlink()
        src.rmdir()
        assert not src.exists()

        # Restore
        restored = engine.restore(manifest.snapshot_id)
        assert restored.snapshot_id == manifest.snapshot_id
        assert src.exists()
        assert (src / "settings.yaml").read_text() == "mode: test"

    def test_restore_latest(self, tmp_path: Path):
        src = tmp_path / "data"
        src.mkdir()
        (src / "a.txt").write_text("v1")

        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        ts1 = datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
        ts2 = datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc)
        engine.create("ag", [str(src)], timestamp=ts1)
        m2 = engine.create("ag", [str(src)], timestamp=ts2)

        restored = engine.restore(latest=True)
        assert restored.snapshot_id == m2.snapshot_id

    def test_restore_no_snapshot_raises(self, tmp_path: Path):
        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        with pytest.raises(BackupError):
            engine.restore(latest=True)


class TestBackupIntegrity:
    def test_verify_valid(self, tmp_path: Path):
        src = tmp_path / "src"
        src.mkdir()
        (src / "f.txt").write_text("data")

        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        manifest = engine.create("int-agent", [str(src)])
        assert engine.verify(manifest.snapshot_id) is True

    def test_verify_tampered_raises(self, tmp_path: Path):
        src = tmp_path / "src"
        src.mkdir()
        (src / "f.txt").write_text("data")

        engine = BackupEngine(snapshot_dir=tmp_path / "snapshots")
        manifest = engine.create("int-agent", [str(src)])

        # Tamper with the archive
        archive = tmp_path / "snapshots" / f"{manifest.snapshot_id}.tar.gz"
        with open(archive, "ab") as f:
            f.write(b"TAMPERED")

        with pytest.raises(IntegrityError, match="FAILED"):
            engine.verify(manifest.snapshot_id)


# ═══════════════════════════════════════════════════════════════
# Phase 2: Setup Loader Tests
# ═══════════════════════════════════════════════════════════════


def _write_plugin_yaml(path: Path, content: str) -> Path:
    """Helper: write a YAML/JSON config file and return its path."""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(textwrap.dedent(content))
    return path


class TestSetupLoaderParsing:
    def test_parse_json_fallback(self, tmp_path: Path):
        """When PyYAML is unavailable, JSON files should still parse."""
        cfg_path = tmp_path / "plugin.json"
        cfg_path.write_text(
            json.dumps(
                {
                    "metadata": {"name": "test", "version": "0.1.0"},
                    "agent": {"default": "self", "config": {"self": {}}},
                    "backup": {"paths": ["/tmp/test"]},
                    "execution": {"benchmarks": []},
                    "evaluation": {},
                    "output": {"results_dir": str(tmp_path / "results")},
                }
            )
        )

        loader = SetupLoader(plugin_path=cfg_path)
        config = loader.parse_only()
        assert config.metadata["name"] == "test"
        assert config.default_agent == "self"

    def test_parse_yaml(self, tmp_path: Path):
        """Parse a real YAML file (requires PyYAML)."""
        pytest.importorskip("yaml")
        cfg_path = tmp_path / "plugin.yaml"
        cfg_path.write_text(
            textwrap.dedent("""\
            metadata:
              name: yaml-test
              version: "0.1.0"
            agent:
              default: self
              config:
                self: {}
            backup:
              paths:
                - /tmp/test
            execution:
              benchmarks: []
            evaluation: {}
            output:
              results_dir: ./results
        """)
        )

        loader = SetupLoader(plugin_path=cfg_path)
        config = loader.parse_only()
        assert config.metadata["name"] == "yaml-test"

    def test_missing_file_raises(self, tmp_path: Path):
        loader = SetupLoader(plugin_path=tmp_path / "nonexistent.yaml")
        with pytest.raises(SetupError, match="not found"):
            loader.parse_only()


class TestEnvVarResolution:
    def test_resolve_existing_var(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ):
        monkeypatch.setenv("TEST_API_KEY", "sk-secret-123")

        cfg_path = tmp_path / "plugin.json"
        cfg_path.write_text(
            json.dumps(
                {
                    "metadata": {"name": "env-test"},
                    "agent": {
                        "default": "self",
                        "config": {
                            "self": {},
                            "custom": {"api_key": "${TEST_API_KEY}"},
                        },
                    },
                    "backup": {},
                    "execution": {},
                    "evaluation": {},
                    "output": {},
                }
            )
        )

        loader = SetupLoader(plugin_path=cfg_path)
        config = loader.parse_only()
        assert config.agent_configs["custom"]["api_key"] == "sk-secret-123"

    def test_missing_env_var_raises(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ):
        # Ensure the var does NOT exist
        monkeypatch.delenv("NONEXISTENT_VAR_XYZ", raising=False)

        cfg_path = tmp_path / "plugin.json"
        cfg_path.write_text(
            json.dumps(
                {
                    "metadata": {},
                    "agent": {
                        "default": "self",
                        "config": {"x": {"key": "${NONEXISTENT_VAR_XYZ}"}},
                    },
                    "backup": {},
                    "execution": {},
                    "evaluation": {},
                    "output": {},
                }
            )
        )

        loader = SetupLoader(plugin_path=cfg_path)
        with pytest.raises(EnvVarMissingError, match="NONEXISTENT_VAR_XYZ"):
            loader.parse_only()

    def test_multiple_vars_in_one_string(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ):
        monkeypatch.setenv("HOST", "localhost")
        monkeypatch.setenv("PORT", "9000")

        cfg_path = tmp_path / "plugin.json"
        cfg_path.write_text(
            json.dumps(
                {
                    "metadata": {},
                    "agent": {
                        "default": "self",
                        "config": {
                            "self": {},
                            "svc": {"url": "http://${HOST}:${PORT}"},
                        },
                    },
                    "backup": {},
                    "execution": {},
                    "evaluation": {},
                    "output": {},
                }
            )
        )

        loader = SetupLoader(plugin_path=cfg_path)
        config = loader.parse_only()
        assert config.agent_configs["svc"]["url"] == "http://localhost:9000"


class TestAdapterValidation:
    """Tests that require adapter registry imports."""

    def test_load_with_self_adapter(self, tmp_path: Path):
        """'self' adapter (NanoClaw) should always be importable and valid."""
        # Import to trigger registration
        import deskclaw.adapters.nanoclaw  # noqa: F401

        results_dir = tmp_path / "results"
        cfg_path = tmp_path / "plugin.json"
        cfg_path.write_text(
            json.dumps(
                {
                    "metadata": {"name": "adapter-test"},
                    "agent": {"default": "self", "config": {"self": {}}},
                    "backup": {"paths": []},
                    "execution": {
                        "benchmarks": [
                            {"name": "stress-tests", "type": "custom"},
                        ],
                        "parallel": {"max_workers": 2},
                        "mock": {"enabled": False},
                    },
                    "evaluation": {},
                    "output": {"results_dir": str(results_dir)},
                }
            )
        )

        loader = SetupLoader(plugin_path=cfg_path)
        session = loader.load(results_base=results_dir)

        assert session.agent == "self"
        assert "stress-tests" in session.benchmarks_loaded
        assert session.parallel_workers == 2
        assert not session.mock_enabled

        # session_config.json should be written
        out = results_dir / session.snapshot_id / "session_config.json"
        assert out.exists()
        data = json.loads(out.read_text())
        assert data["agent"] == "self"

    def test_unknown_adapter_raises(self, tmp_path: Path):
        cfg_path = tmp_path / "plugin.json"
        cfg_path.write_text(
            json.dumps(
                {
                    "metadata": {},
                    "agent": {"default": "nonexistent-adapter-xyz", "config": {}},
                    "backup": {},
                    "execution": {},
                    "evaluation": {},
                    "output": {"results_dir": str(tmp_path / "r")},
                }
            )
        )

        loader = SetupLoader(plugin_path=cfg_path)
        with pytest.raises(AdapterValidationError, match="not registered"):
            loader.load(results_base=tmp_path / "r")


class TestSessionConfigOutput:
    def test_session_config_fields(self, tmp_path: Path):
        import deskclaw.adapters.nanoclaw  # noqa: F401

        results_dir = tmp_path / "results"
        cfg_path = tmp_path / "plugin.json"
        cfg_path.write_text(
            json.dumps(
                {
                    "metadata": {"name": "fields-test"},
                    "agent": {"default": "self", "config": {"self": {}}},
                    "backup": {},
                    "execution": {
                        "benchmarks": [{"name": "a"}, {"name": "b"}],
                        "parallel": {"max_workers": 8},
                        "mock": {"enabled": True},
                    },
                    "evaluation": {},
                    "output": {"results_dir": str(results_dir)},
                }
            )
        )

        loader = SetupLoader(plugin_path=cfg_path)
        session = loader.load(
            snapshot_id="test_snap_001",
            results_base=results_dir,
        )

        assert session.snapshot_id == "test_snap_001"
        assert session.benchmarks_loaded == ["a", "b"]
        assert session.parallel_workers == 8
        assert session.mock_enabled is True
        assert len(session.plugin_yaml_sha256) == 64

        # Verify JSON on disk
        data = json.loads(
            (results_dir / "test_snap_001" / "session_config.json").read_text()
        )
        assert data["snapshot_id"] == "test_snap_001"
        assert data["mock_enabled"] is True
