"""Tests for lib/taxonomy.py — asset naming convention and slot mapping."""

import sys
from pathlib import Path
import tempfile

# Ensure starsend root is on the path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from recoil.pipeline._lib.taxonomy import (
    parse_asset_filename,
    AssetId,
    AssetNameError,
    ASSET_FILENAME_RE,
    VALID_TYPES,
    SLOT_MAP,
    SlotSpec,
    build_asset_filename,
    validate_ref_weight,
    validate_ref_auto,
    default_weight,
    is_valid_asset_filename,
    normalize_extension,
    next_version,
)

import pytest


# ======================================================================
# Valid filenames — should all parse
# ======================================================================

class TestValidFilenames:
    """Test that all valid naming patterns parse correctly."""

    def test_identity_hero(self):
        aid = parse_asset_filename("torch_identity_hero_v01.png")
        assert aid == AssetId("torch", "identity", "hero", 1, "png")

    def test_turn_front(self):
        aid = parse_asset_filename("torch_turn_front_v01.png")
        assert aid.type == "turn"
        assert aid.variant == "front"

    def test_turn_three_quarter(self):
        aid = parse_asset_filename("torch_turn_three-quarter_v02.png")
        assert aid.variant == "three-quarter"
        assert aid.version == 2

    def test_expression(self):
        aid = parse_asset_filename("torch_expr_anguish_v01.png")
        assert aid.type == "expr"

    def test_location_wide(self):
        aid = parse_asset_filename("int-lower-decks_loc_wide_v01.png")
        assert aid.subject == "int-lower-decks"
        assert aid.type == "loc"
        assert aid.variant == "wide"

    def test_location_detail(self):
        aid = parse_asset_filename("int-lower-decks_loc_pipe-detail_v03.png")
        assert aid.variant == "pipe-detail"
        assert aid.version == 3

    def test_prop(self):
        aid = parse_asset_filename("plasma-cutter_prop_hero_v01.png")
        assert aid.subject == "plasma-cutter"
        assert aid.type == "prop"

    def test_scene(self):
        aid = parse_asset_filename("torch_scene_lower-decks_v01.jpeg")
        assert aid.type == "scene"
        assert aid.ext == "jpeg"

    def test_webp_extension(self):
        aid = parse_asset_filename("torch_identity_hero_v01.webp")
        assert aid.ext == "webp"

    def test_version_99(self):
        aid = parse_asset_filename("torch_identity_hero_v99.png")
        assert aid.version == 99

    def test_version_00(self):
        aid = parse_asset_filename("torch_identity_hero_v00.png")
        assert aid.version == 0

    def test_all_six_types(self):
        for t in VALID_TYPES:
            name = f"test_{t}_variant_v01.png"
            aid = parse_asset_filename(name)
            assert aid.type == t


# ======================================================================
# Extension normalization
# ======================================================================

class TestExtensionNormalization:
    """Test .jpg -> .jpeg normalization on ingest."""

    def test_jpg_normalized_to_jpeg(self):
        aid = parse_asset_filename("torch_identity_hero_v01.jpg")
        assert aid.ext == "jpeg"

    def test_jpeg_stays_jpeg(self):
        aid = parse_asset_filename("torch_identity_hero_v01.jpeg")
        assert aid.ext == "jpeg"

    def test_normalize_extension_function(self):
        assert normalize_extension("file.jpg") == "file.jpeg"
        assert normalize_extension("file.jpeg") == "file.jpeg"
        assert normalize_extension("file.png") == "file.png"
        assert normalize_extension("file.tif") == "file.tiff"

    def test_normalize_disabled(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("torch_identity_hero_v01.jpg", normalize=False)


# ======================================================================
# Invalid filenames — should all raise AssetNameError
# ======================================================================

class TestInvalidFilenames:
    """Test that invalid patterns are rejected."""

    def test_path_rejected(self):
        with pytest.raises(AssetNameError, match="bare filename"):
            parse_asset_filename("path/to/torch_identity_hero_v01.png")

    def test_backslash_path_rejected(self):
        with pytest.raises(AssetNameError, match="bare filename"):
            parse_asset_filename("path\\torch_identity_hero_v01.png")

    def test_uppercase_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("TORCH_identity_hero_v01.png")

    def test_spaces_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("bad file.png")

    def test_invalid_type_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("torch_hero_neutral_v01.png")

    def test_missing_version_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("torch_identity_hero.png")

    def test_single_digit_version_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("torch_identity_hero_v1.png")

    def test_three_digit_version_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("torch_identity_hero_v001.png")

    def test_wrong_extension_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("torch_identity_hero_v01.gif", normalize=False)

    def test_extra_segments_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("char_torch_identity_hero_v01.png")

    def test_underscore_in_subject_rejected(self):
        """Underscores in subject would create ambiguous segment boundaries."""
        with pytest.raises(AssetNameError):
            parse_asset_filename("int_lower_decks_loc_wide_v01.png")

    def test_empty_string_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("")

    def test_subject_starts_with_number_rejected(self):
        with pytest.raises(AssetNameError):
            parse_asset_filename("7sector_loc_wide_v01.png")


# ======================================================================
# AssetId methods
# ======================================================================

class TestAssetId:
    """Test AssetId dataclass methods."""

    def test_filename_roundtrip(self):
        aid = parse_asset_filename("torch_identity_hero_v01.png")
        assert aid.filename == "torch_identity_hero_v01.png"
        # Parse the reconstructed filename
        aid2 = parse_asset_filename(aid.filename)
        assert aid == aid2

    def test_bump_increments_version(self):
        aid = parse_asset_filename("torch_identity_hero_v03.png")
        bumped = aid.bump()
        assert bumped.version == 4
        assert bumped.subject == "torch"
        assert bumped.filename == "torch_identity_hero_v04.png"

    def test_frozen(self):
        aid = parse_asset_filename("torch_identity_hero_v01.png")
        with pytest.raises(AttributeError):
            aid.version = 2  # type: ignore

    def test_is_valid_helper(self):
        assert is_valid_asset_filename("torch_identity_hero_v01.png")
        assert not is_valid_asset_filename("bad file.png")
        assert not is_valid_asset_filename("")


# ======================================================================
# Slot mapping
# ======================================================================

class TestSlotMap:
    """Test pipeline slot mapping constants."""

    def test_six_types_mapped(self):
        assert set(SLOT_MAP.keys()) == VALID_TYPES

    def test_scene_not_auto_eligible(self):
        assert SLOT_MAP["scene"].auto_eligible is False

    def test_all_others_auto_eligible(self):
        for t in ("identity", "turn", "expr", "loc", "prop"):
            assert SLOT_MAP[t].auto_eligible is True, f"{t} should be auto-eligible"

    def test_identity_highest_weight(self):
        assert SLOT_MAP["identity"].weight_max == 10

    def test_loc_lowest_weight(self):
        assert SLOT_MAP["loc"].weight_min == 1

    def test_weight_validation_in_range(self):
        assert validate_ref_weight("identity", 9) == []

    def test_weight_validation_below_range(self):
        warnings = validate_ref_weight("identity", 1)
        assert len(warnings) == 1
        assert "below" in warnings[0]

    def test_weight_validation_above_range(self):
        warnings = validate_ref_weight("loc", 10)
        assert len(warnings) == 1
        assert "above" in warnings[0]

    def test_auto_validation_scene_true_rejected(self):
        errors = validate_ref_auto("scene", True)
        assert len(errors) == 1

    def test_auto_validation_scene_false_ok(self):
        assert validate_ref_auto("scene", False) == []

    def test_auto_validation_identity_true_ok(self):
        assert validate_ref_auto("identity", True) == []

    def test_default_weight(self):
        assert default_weight("identity") == 9  # (8+10)//2
        assert default_weight("loc") == 1  # (1+2)//2


# ======================================================================
# Filename construction
# ======================================================================

class TestBuildFilename:
    """Test build_asset_filename() helper."""

    def test_basic_build(self):
        name = build_asset_filename("torch", "identity", "hero", version=1)
        assert name == "torch_identity_hero_v01.png"

    def test_invalid_type_rejected(self):
        with pytest.raises(AssetNameError):
            build_asset_filename("torch", "hero", "neutral")

    def test_roundtrip(self):
        name = build_asset_filename("int-lower-decks", "loc", "pipe-detail", version=3, ext="jpeg")
        aid = parse_asset_filename(name)
        assert aid.subject == "int-lower-decks"
        assert aid.version == 3


# ======================================================================
# next_version()
# ======================================================================

class TestNextVersion:
    """Test auto-version detection in a directory."""

    def test_empty_dir_returns_1(self, tmp_path):
        assert next_version(tmp_path, "torch", "identity", "hero") == 1

    def test_increments_from_existing(self, tmp_path):
        (tmp_path / "torch_identity_hero_v01.png").touch()
        (tmp_path / "torch_identity_hero_v03.png").touch()
        assert next_version(tmp_path, "torch", "identity", "hero") == 4

    def test_ignores_other_subjects(self, tmp_path):
        (tmp_path / "wren_identity_hero_v05.png").touch()
        assert next_version(tmp_path, "torch", "identity", "hero") == 1

    def test_ignores_invalid_files(self, tmp_path):
        (tmp_path / "old_style_hero.png").touch()
        (tmp_path / "torch_identity_hero_v02.png").touch()
        assert next_version(tmp_path, "torch", "identity", "hero") == 3
