"""Unit tests for ArtifactRegistry: hash_tree, register, list, diff, persistence."""

from __future__ import annotations

import json
import os
from datetime import datetime
from pathlib import Path

import pytest

from vmcluster_mcp.artifact.registry import ArtifactRegistry, hash_tree
from vmcluster_mcp.models import ArtifactDiff, BuildArtifact, ToolResult


# -- Helpers --


def make_tree(base: Path, files: dict[str, bytes]) -> Path:
    """Create a temp directory tree with given {relative_path: content} files."""
    base.mkdir(parents=True, exist_ok=True)
    for rel, content in files.items():
        p = base / rel
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_bytes(content)
    return base


def make_elf(base: Path, rel_path: str) -> None:
    """Write a minimal ELF magic header file."""
    p = base / rel_path
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_bytes(b"\x7fELF" + b"\x00" * 60)


# -- hash_tree tests --


def test_hash_tree_same_content_same_hash(tmp_path: Path) -> None:
    tree_a = tmp_path / "a"
    tree_b = tmp_path / "b"
    files = {"foo/bar.c": b"hello", "baz.txt": b"world"}
    make_tree(tree_a, files)
    make_tree(tree_b, files)
    assert hash_tree(str(tree_a)) == hash_tree(str(tree_b))


def test_hash_tree_different_content_different_hash(tmp_path: Path) -> None:
    tree_a = tmp_path / "a"
    tree_b = tmp_path / "b"
    make_tree(tree_a, {"file.c": b"version1"})
    make_tree(tree_b, {"file.c": b"version2"})
    assert hash_tree(str(tree_a)) != hash_tree(str(tree_b))


def test_hash_tree_excludes_pycache(tmp_path: Path) -> None:
    tree_a = tmp_path / "a"
    tree_b = tmp_path / "b"
    for tree in (tree_a, tree_b):
        make_tree(tree, {"main.c": b"code"})
    # Different __pycache__ content in b only
    pycache_dir = tree_b / "__pycache__"
    pycache_dir.mkdir()
    (pycache_dir / "cachefile.pyc").write_bytes(b"different")
    assert hash_tree(str(tree_a)) == hash_tree(str(tree_b))


def test_hash_tree_excludes_git(tmp_path: Path) -> None:
    tree_a = tmp_path / "a"
    tree_b = tmp_path / "b"
    for tree in (tree_a, tree_b):
        make_tree(tree, {"driver.c": b"code"})
    # Different .git content in b only
    git_dir = tree_b / ".git"
    git_dir.mkdir()
    (git_dir / "COMMIT_EDITMSG").write_bytes(b"abc123")
    assert hash_tree(str(tree_a)) == hash_tree(str(tree_b))


# -- ArtifactRegistry: load/save tests --


def test_load_missing_registry(tmp_path: Path) -> None:
    reg = ArtifactRegistry(str(tmp_path / "sub" / "registry.json"))
    reg.load()
    assert reg.list_artifacts() == []


def test_save_atomic_write(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """save() must write .tmp then os.replace — never in-place overwrite."""
    registry_path = str(tmp_path / "registry.json")
    reg = ArtifactRegistry(registry_path)
    reg.load()

    replace_calls: list[tuple[str, str]] = []
    real_replace = os.replace

    def spy_replace(src: str, dst: str) -> None:
        replace_calls.append((src, dst))
        real_replace(src, dst)

    monkeypatch.setattr(os, "replace", spy_replace)

    tree = tmp_path / "tree"
    make_tree(tree, {"driver.ko": b"ko_data"})
    reg.register(str(tree), "raw_tree", kernel_version="6.6.0")

    assert len(replace_calls) == 1
    src_path, dst_path = replace_calls[0]
    assert src_path.endswith(".tmp")
    assert dst_path == registry_path


def test_load_reads_existing_registry(tmp_path: Path) -> None:
    registry_path = tmp_path / "registry.json"
    tree = tmp_path / "tree"
    make_tree(tree, {"driver.ko": b"ko"})

    reg = ArtifactRegistry(str(registry_path))
    reg.load()
    artifact = reg.register(str(tree), "raw_tree", kernel_version="6.6.0")
    artifact_id = artifact.artifact_id

    # New instance loads from disk
    reg2 = ArtifactRegistry(str(registry_path))
    reg2.load()
    loaded = reg2.get(artifact_id)
    assert loaded is not None
    assert loaded.artifact_id == artifact_id


# -- ArtifactRegistry: register tests --


def test_register_returns_build_artifact(tmp_path: Path) -> None:
    tree = tmp_path / "build"
    make_tree(tree, {"drivers/mymod.ko": b"ko_bytes"})
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()

    artifact = reg.register(str(tree), "raw_tree", kernel_version="5.15.0")

    assert isinstance(artifact, BuildArtifact)
    assert artifact.artifact_id == hash_tree(str(tree))
    assert artifact.source_path == str(tree)
    assert artifact.build_type == "raw_tree"
    assert artifact.kernel_version == "5.15.0"
    assert "drivers/mymod.ko" in artifact.modules


def test_register_iso8601_timestamp(tmp_path: Path) -> None:
    tree = tmp_path / "build"
    make_tree(tree, {"a.ko": b"data"})
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()

    artifact = reg.register(str(tree), "raw_tree", kernel_version="5.15.0")
    parsed = datetime.fromisoformat(artifact.build_timestamp)
    assert parsed.tzinfo is not None  # timezone-aware


def test_register_scans_ko_modules(tmp_path: Path) -> None:
    tree = tmp_path / "build"
    make_tree(tree, {
        "drivers/net/mynet.ko": b"ko1",
        "drivers/block/blk.ko": b"ko2",
        "Makefile": b"make",
    })
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()

    artifact = reg.register(str(tree), "raw_tree", kernel_version="5.15.0")
    assert sorted(artifact.modules) == ["drivers/block/blk.ko", "drivers/net/mynet.ko"]


def test_register_scans_elf_bins(tmp_path: Path) -> None:
    tree = tmp_path / "build"
    tree.mkdir()
    make_elf(tree, "tools/mytool")
    make_elf(tree, "lib/helper")
    make_tree(tree, {"README": b"text"})
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()

    artifact = reg.register(str(tree), "raw_tree", kernel_version="5.15.0")
    assert "tools/mytool" in artifact.userspace_bins
    assert "lib/helper" in artifact.userspace_bins
    assert "README" not in artifact.userspace_bins


def test_register_dedup_same_tree(tmp_path: Path) -> None:
    tree = tmp_path / "build"
    make_tree(tree, {"a.ko": b"same"})
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()

    artifact1 = reg.register(str(tree), "raw_tree", kernel_version="6.1.0")
    artifact2 = reg.register(str(tree), "raw_tree", kernel_version="6.1.0")
    assert artifact1.artifact_id == artifact2.artifact_id
    assert len(reg.list_artifacts()) == 1


def test_register_autodetects_kernel_version(tmp_path: Path) -> None:
    tree = tmp_path / "build"
    utsrelease = tree / "include" / "generated" / "utsrelease.h"
    utsrelease.parent.mkdir(parents=True, exist_ok=True)
    utsrelease.write_text('#define UTS_RELEASE "6.8.0-custom"\n')
    make_tree(tree, {"driver.ko": b"ko"})

    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()
    artifact = reg.register(str(tree), "raw_tree")  # No kernel_version provided
    assert artifact.kernel_version == "6.8.0-custom"


def test_register_uses_caller_kernel_version_when_no_utsrelease(tmp_path: Path) -> None:
    tree = tmp_path / "build"
    make_tree(tree, {"driver.ko": b"ko"})
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()

    artifact = reg.register(str(tree), "raw_tree", kernel_version="5.4.200")
    assert artifact.kernel_version == "5.4.200"


# -- ArtifactRegistry: list_artifacts tests --


def test_list_artifacts_empty(tmp_path: Path) -> None:
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()
    assert reg.list_artifacts() == []


def test_list_artifacts_returns_all(tmp_path: Path) -> None:
    tree_a = tmp_path / "a"
    tree_b = tmp_path / "b"
    make_tree(tree_a, {"a.ko": b"data_a"})
    make_tree(tree_b, {"b.ko": b"data_b"})
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()

    reg.register(str(tree_a), "raw_tree", kernel_version="6.1.0")
    reg.register(str(tree_b), "raw_tree", kernel_version="6.2.0")

    artifacts = reg.list_artifacts()
    assert len(artifacts) == 2
    assert all(isinstance(a, BuildArtifact) for a in artifacts)


# -- ArtifactRegistry: diff tests --


def test_diff_returns_artifact_diff(tmp_path: Path) -> None:
    tree_a = tmp_path / "a"
    tree_b = tmp_path / "b"
    make_tree(tree_a, {
        "drivers/driver.ko": b"same_content",
        "drivers/net.ko": b"v1_content",
        "tools/helper.txt": b"text_a",
    })
    tree_b.mkdir(parents=True, exist_ok=True)
    make_elf(tree_b, "tools/mytool")
    make_tree(tree_b, {
        "drivers/driver.ko": b"same_content",
        "drivers/net.ko": b"v2_content",
        "tools/helper.txt": b"text_b",
    })

    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()
    art_a = reg.register(str(tree_a), "raw_tree", kernel_version="6.1.0")
    art_b = reg.register(str(tree_b), "raw_tree", kernel_version="6.1.0")

    result = reg.diff(art_a.artifact_id, art_b.artifact_id)

    assert isinstance(result, ArtifactDiff)
    assert result.artifact_id_a == art_a.artifact_id
    assert result.artifact_id_b == art_b.artifact_id
    assert "drivers/net.ko" in result.changed_modules
    assert "drivers/driver.ko" not in result.changed_modules
    # changed bins (text_a vs text_b or ELF in b but not in a)
    assert isinstance(result.changed_bins, list)


def test_diff_unchanged_artifacts(tmp_path: Path) -> None:
    """Diffing two registrations of the same tree returns empty changed lists."""
    tree = tmp_path / "build"
    make_tree(tree, {"drivers/mod.ko": b"ko_data"})

    # Register same tree with different source paths but same content
    tree_copy = tmp_path / "build_copy"
    make_tree(tree_copy, {"drivers/mod.ko": b"ko_data"})

    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()
    art_a = reg.register(str(tree), "raw_tree", kernel_version="6.0.0")
    art_b = reg.register(str(tree_copy), "raw_tree", kernel_version="6.0.0")

    # Same content → same artifact_id (dedup)
    assert art_a.artifact_id == art_b.artifact_id


def test_diff_unknown_artifact_id(tmp_path: Path) -> None:
    reg = ArtifactRegistry(str(tmp_path / "registry.json"))
    reg.load()

    result = reg.diff("nonexistent_a", "nonexistent_b")

    assert isinstance(result, ToolResult)
    assert result.success is False
    assert result.error is not None
    assert result.error.code == "ARTIFACT_NOT_FOUND"
