"""Tests for ProjectPaths.resolve_ref() — the v3 SSOT ref resolver.

Covers: alias collapse, subject normalization, variant priority, version glob,
integrity check (small file, no header, missing file), RefNotFoundError vs
BrokenRefError, validate=False opt-out, all valid asset kinds.
"""
from pathlib import Path
import pytest

from recoil.core.paths import (
    ProjectPaths,
    RefNotFoundError,
    BrokenRefError,
    VALID_ASSET_CLASSES,
    VALID_REF_TYPES,
    _normalize_subject,
    _normalize_class,
    _candidate_stems,
    _integrity_check,
)

# ── Minimal valid image bytes ──────────────────────────────────────

# Minimal PNG: 8-byte header + IHDR chunk (enough to pass magic + size checks)
_MINIMAL_PNG = (
    b"\x89PNG\r\n\x1a\n"
    b"\x00\x00\x00\rIHDR"
    b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89"
    b"\x00\x00\x00\rIDATx\x9cb\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4"
    b"\x00\x00\x00\x00IEND\xaeB`\x82"
)
# Pad to > 1024 bytes
VALID_PNG = _MINIMAL_PNG + b"\x00" * (1100 - len(_MINIMAL_PNG))

# Minimal JPEG header
VALID_JPEG = b"\xff\xd8\xff\xe0" + b"\x00" * 1100


def _make_project(tmp_path: Path, slug: str = "testproj") -> ProjectPaths:
    """Create a minimal project directory structure and return ProjectPaths."""
    proj = tmp_path / slug
    proj.mkdir()
    (proj / "project_config.json").write_text("{}")
    (proj / "assets").mkdir()
    for cls in ("char", "loc", "prop"):
        (proj / "assets" / cls).mkdir()
    return ProjectPaths(project_root=proj)


def _write_ref(paths: ProjectPaths, cls: str, subject: str,
               filename: str, content: bytes = VALID_PNG,
               look: str = "base") -> Path:
    """Write a ref file into the look directory. Returns the file path."""
    look_dir = paths.asset_look_dir(cls, subject, look)
    look_dir.mkdir(parents=True, exist_ok=True)
    p = look_dir / filename
    p.write_bytes(content)
    return p


# ── _normalize_subject ─────────────────────────────────────────────

class TestNormalizeSubject:
    def test_lowercase(self):
        assert _normalize_subject("JADE") == "jade"

    def test_hyphens_to_underscores(self):
        assert _normalize_subject("int-lower-decks-corridor") == "int_lower_decks_corridor"

    def test_strip_whitespace(self):
        assert _normalize_subject("  jade  ") == "jade"

    def test_empty_raises(self):
        with pytest.raises(ValueError, match="non-empty"):
            _normalize_subject("")


# ── _normalize_class ───────────────────────────────────────────────

class TestNormalizeClass:
    def test_canonical_passes_through(self):
        for cls in VALID_ASSET_CLASSES:
            assert _normalize_class(cls) == cls

    def test_identity_alias(self):
        assert _normalize_class("identity") == "char"

    def test_character_alias(self):
        assert _normalize_class("character") == "char"

    def test_characters_alias(self):
        assert _normalize_class("characters") == "char"

    def test_location_alias(self):
        assert _normalize_class("location") == "loc"

    def test_invalid_raises(self):
        with pytest.raises(ValueError, match="Invalid asset class"):
            _normalize_class("bogus")


# ── _candidate_stems ──────────────────────────────────────────────

class TestCandidateStems:
    def test_with_variant(self):
        stems = _candidate_stems("jade", "identity", "hero")
        assert stems == ["jade_identity_hero", "jade_identity", "jade-identity"]

    def test_no_variant(self):
        stems = _candidate_stems("jade", "identity", None)
        assert stems == ["jade_identity_hero", "jade_identity", "jade-identity"]

    def test_explicit_variant(self):
        stems = _candidate_stems("jade", "identity", "profile")
        # Explicit non-hero variant: tries the variant stem then bare stem
        assert stems == ["jade_identity_profile", "jade_identity", "jade-identity"]

    def test_dedup(self):
        stems = _candidate_stems("jade", "identity", "hero")
        assert len(stems) == len(set(stems))


# ── _integrity_check ──────────────────────────────────────────────

class TestIntegrityCheck:
    def test_valid_png_passes(self, tmp_path):
        p = tmp_path / "test.png"
        p.write_bytes(VALID_PNG)
        _integrity_check(p)  # should not raise

    def test_valid_jpeg_passes(self, tmp_path):
        p = tmp_path / "test.jpeg"
        p.write_bytes(VALID_JPEG)
        _integrity_check(p)

    def test_small_file_raises(self, tmp_path):
        p = tmp_path / "broken.png"
        p.write_bytes(b"version https://git-lfs.github.com/spec/v1\n")
        with pytest.raises(BrokenRefError, match="too small"):
            _integrity_check(p)

    def test_no_magic_raises(self, tmp_path):
        p = tmp_path / "fake.png"
        p.write_bytes(b"NOT AN IMAGE" + b"\x00" * 1100)
        with pytest.raises(BrokenRefError, match="no image header magic"):
            _integrity_check(p)

    def test_missing_file_raises(self, tmp_path):
        p = tmp_path / "ghost.png"
        with pytest.raises(BrokenRefError, match="Cannot stat"):
            _integrity_check(p)


# ── resolve_ref integration tests ─────────────────────────────────

class TestResolveRef:
    def test_exact_match(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity.png")
        ref = paths.resolve_ref("char", "jade", "identity")
        assert ref.path.name == "jade_identity.png"
        assert ref.cls == "char"
        assert ref.subject == "jade"
        assert ref.kind == "identity"
        assert ref.version is None
        assert ref.integrity_ok is True

    def test_versioned_glob_picks_highest(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity_hero_v01.png")
        _write_ref(paths, "char", "jade", "jade_identity_hero_v03.png")
        _write_ref(paths, "char", "jade", "jade_identity_hero_v02.png")
        ref = paths.resolve_ref("char", "jade", "identity", variant="hero")
        assert ref.path.name == "jade_identity_hero_v03.png"
        assert ref.version == 3

    def test_alias_identity_to_char(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity_hero.png")
        ref = paths.resolve_ref("identity", "jade", "identity", variant="hero")
        assert ref.cls == "char"
        assert ref.path.exists()

    def test_subject_normalization_hyphens(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "loc", "int_lower_decks", "int_lower_decks_identity.png")
        ref = paths.resolve_ref("loc", "int-lower-decks", "identity")
        assert ref.subject == "int_lower_decks"

    def test_not_found_raises(self, tmp_path):
        paths = _make_project(tmp_path)
        (paths.asset_look_dir("char", "ghost", "base")).mkdir(parents=True)
        with pytest.raises(RefNotFoundError, match="No ref found"):
            paths.resolve_ref("char", "ghost", "identity")

    def test_no_look_dir_raises(self, tmp_path):
        paths = _make_project(tmp_path)
        with pytest.raises(RefNotFoundError, match="No look directory"):
            paths.resolve_ref("char", "nonexistent", "identity")

    def test_broken_file_raises(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity.png",
                   content=b"broken lfs pointer stub text")
        with pytest.raises(BrokenRefError, match="too small"):
            paths.resolve_ref("char", "jade", "identity")

    def test_validate_false_skips_integrity(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity.png",
                   content=b"x" * 10)  # too small for integrity
        ref = paths.resolve_ref("char", "jade", "identity", validate=False)
        assert ref.path.exists()
        assert ref.integrity_ok is True  # always True in ResolvedRef

    def test_jpeg_preferred_over_png(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity.jpeg", content=VALID_JPEG)
        _write_ref(paths, "char", "jade", "jade_identity.png")
        ref = paths.resolve_ref("char", "jade", "identity")
        assert ref.path.suffix == ".jpeg"

    def test_variant_priority_explicit_over_hero(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity_profile.png")
        _write_ref(paths, "char", "jade", "jade_identity_hero.png")
        ref = paths.resolve_ref("char", "jade", "identity", variant="profile")
        assert "profile" in ref.path.name

    def test_invalid_kind_raises(self, tmp_path):
        paths = _make_project(tmp_path)
        with pytest.raises(ValueError, match="Invalid ref kind"):
            paths.resolve_ref("char", "jade", "bogus_kind")

    def test_invalid_class_raises(self, tmp_path):
        paths = _make_project(tmp_path)
        with pytest.raises(ValueError, match="Invalid asset class"):
            paths.resolve_ref("bogus_class", "jade", "identity")

    def test_all_valid_classes(self, tmp_path):
        """Each valid class can resolve."""
        paths = _make_project(tmp_path)
        for cls in VALID_ASSET_CLASSES:
            _write_ref(paths, cls, "test_subj", "test_subj_identity.png")
            ref = paths.resolve_ref(cls, "test_subj", "identity")
            assert ref.cls == cls

    def test_all_valid_ref_types(self, tmp_path):
        paths = _make_project(tmp_path)
        for kind in VALID_REF_TYPES:
            _write_ref(paths, "char", "jade", f"jade_{kind}.png")
            ref = paths.resolve_ref("char", "jade", kind)
            assert ref.kind == kind


# ── resolve_hero back-compat ──────────────────────────────────────

class TestResolveHero:
    def test_returns_path_not_resolved_ref(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity_hero.png")
        result = paths.resolve_hero("char", "jade", "identity")
        assert isinstance(result, Path)

    def test_finds_versioned_hero(self, tmp_path):
        paths = _make_project(tmp_path)
        _write_ref(paths, "char", "jade", "jade_identity_hero_v01.png")
        result = paths.resolve_hero("char", "jade", "identity")
        assert result.name == "jade_identity_hero_v01.png"
