"""Tests for godotiq_animation_audit tool."""

from __future__ import annotations

from godotiq.tools.animation import (
    _build_animation_audit,
    _MAX_ISSUES,
    _SEVERITY_ORDER,
)


class TestAuditBrokenTrack:
    """Test 1: broken_track detects NonExistent:position in attack animation."""

    def test_broken_track_critical(self, anim_session):
        """Attack animation references NonExistent node — should be critical."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/animated_character.tscn"
        )
        broken = [i for i in result["issues"] if i["type"] == "broken_track"]
        assert len(broken) == 1
        assert broken[0]["severity"] == "critical"
        assert broken[0]["animation"] == "attack"
        assert broken[0]["missing_node"] == "NonExistent"


class TestAuditOrphanAnimation:
    """Test 2: orphan_animation detects unreferenced idle_ground."""

    def test_orphan_animation_warning(self, anim_session):
        """idle_ground is defined but never referenced in code — should be warning."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/animated_character.tscn"
        )
        orphans = [i for i in result["issues"] if i["type"] == "orphan_animation"]
        orphan_names = {i["animation"] for i in orphans}
        assert "idle_ground" in orphan_names
        assert all(i["severity"] == "warning" for i in orphans)


class TestAuditMissingAnimation:
    """Test 3: missing_animation detects .play() of non-existent animation."""

    def test_missing_animation_warning(self, anim_session):
        """character.gd calls .play('special_move') which is not in the scene."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/animated_character.tscn"
        )
        missing = [i for i in result["issues"] if i["type"] == "missing_animation"]
        missing_names = {i["animation"] for i in missing}
        assert "special_move" in missing_names
        # special_move is not GLB-sourced, so severity should be warning
        special = next(i for i in missing if i["animation"] == "special_move")
        assert special["severity"] == "warning"


class TestAuditNoLoopSuspect:
    """Test 4: no_loop_suspect detects short idle-like non-looping animation."""

    def test_no_loop_suspect_info(self, anim_session):
        """idle_ground is short (1.5s), no loop, name contains 'idle' — should flag."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/animated_character.tscn"
        )
        suspects = [i for i in result["issues"] if i["type"] == "no_loop_suspect"]
        suspect_names = {i["animation"] for i in suspects}
        assert "idle_ground" in suspect_names
        for s in suspects:
            assert s["severity"] == "info"


class TestAuditEmptyPlayer:
    """Test 5: empty_player detects AnimationPlayer with no animations."""

    def test_empty_player_warning(self, anim_session):
        """empty_player_scene.tscn has AnimationPlayer with no library/animations."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/empty_player_scene.tscn"
        )
        empty = [i for i in result["issues"] if i["type"] == "empty_player"]
        assert len(empty) == 1
        assert empty[0]["severity"] == "warning"
        assert empty[0]["player"] == "AnimationPlayer"


class TestAuditCodeOnly:
    """Test 6: code_only_animations detected for animated_character scene."""

    def test_code_only_in_character_scene(self, anim_session):
        """Character scene has code refs to names not in .tscn (after dedup with missing)."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/animated_character.tscn"
        )
        code_only = [i for i in result["issues"] if i["type"] == "code_only_animations"]
        # code_only_animations are deduplicated against missing_animation (play_calls not in scene)
        # So code_only picks up refs from anim_maps/looping_lists not already in play_calls-missing
        for issue in code_only:
            assert issue["severity"] == "info"


class TestAuditOrphanTreeState:
    """Test 7: orphan_tree_state — verify no false positives on matching fixture."""

    def test_no_orphan_tree_state_when_matched(self, anim_session):
        """animated_scene_tree.tscn has matching tree states and player — no orphan."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/animated_scene_tree.tscn"
        )
        orphan_tree = [i for i in result["issues"] if i["type"] == "orphan_tree_state"]
        assert len(orphan_tree) == 0, (
            f"Expected no orphan_tree_state issues, got: {orphan_tree}"
        )


class TestAuditScopeAll:
    """Test 8: scope='all' scans all scenes in the project."""

    def test_scope_all(self, anim_session):
        """Scanning all scenes should report all 5 fixture scenes."""
        result = _build_animation_audit(anim_session, scope="all")
        assert result["scope"] == "all"
        assert result["scenes_scanned"] == 5
        assert result["animated_scenes"] >= 2
        assert result["total_animations"] >= 6  # 4 from character + 2 from tree scene


class TestAuditScopeScene:
    """Test 9: scope='scene' scans only the specified scene."""

    def test_scope_scene(self, anim_session):
        """Single-scene scope should only scan that scene."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/animated_character.tscn"
        )
        assert result["scope"] == "scene"
        assert result["scenes_scanned"] == 1
        assert result["animated_scenes"] == 1
        for issue in result["issues"]:
            assert issue["scene"] == "res://scenes/animated_character.tscn"

    def test_scope_scene_missing_param(self, anim_session):
        """scope='scene' without scene param should return error."""
        result = _build_animation_audit(anim_session, scope="scene")
        assert "error" in result


class TestAuditCleanScene:
    """Test 10: clean scene with no animations produces zero issues."""

    def test_no_issues_for_static_scene(self, anim_session):
        """no_animation_scene.tscn has no AnimationPlayer — zero issues expected."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/no_animation_scene.tscn"
        )
        assert result["scenes_scanned"] == 1
        assert result["animated_scenes"] == 0
        assert result["total_issues"] == 0
        assert result["issues"] == []


class TestAuditSeverityOrdering:
    """Test 11: issues are sorted critical > warning > info."""

    def test_severity_ordering(self, anim_session):
        """Issues should be sorted by severity: critical first, then warning, then info."""
        result = _build_animation_audit(
            anim_session, scope="scene", scene_filter="res://scenes/animated_character.tscn"
        )
        issues = result["issues"]
        assert len(issues) >= 2, "Need at least 2 issues to verify ordering"
        for i in range(len(issues) - 1):
            cur = _SEVERITY_ORDER[issues[i]["severity"]]
            nxt = _SEVERITY_ORDER[issues[i + 1]["severity"]]
            assert cur <= nxt, (
                f"Issue #{i} ({issues[i]['severity']}) should come before "
                f"#{i+1} ({issues[i+1]['severity']})"
            )


class TestAuditIssueCap:
    """Test 12: issues capped at 50, critical preserved."""

    def test_issue_cap_structure(self, anim_session):
        """by_severity dict should always contain all three levels."""
        result = _build_animation_audit(anim_session, scope="all")
        assert "critical" in result["by_severity"]
        assert "warning" in result["by_severity"]
        assert "info" in result["by_severity"]
        assert result["total_issues"] == len(result["issues"])
        assert "truncated" not in result  # fixture set is small

    def test_issue_cap_truncation_preserves_critical(self):
        """Verify truncation logic preserves critical issues and caps at MAX_ISSUES."""
        # Build a synthetic issues list exceeding the cap
        issues = []
        for i in range(10):
            issues.append({"severity": "critical", "type": "broken_track", "scene": f"s{i}"})
        for i in range(30):
            issues.append({"severity": "warning", "type": "orphan_animation", "scene": f"s{i}"})
        for i in range(30):
            issues.append({"severity": "info", "type": "no_loop_suspect", "scene": f"s{i}"})
        # 70 total > 50 cap

        # Sort by severity (matches implementation)
        issues.sort(key=lambda x: (_SEVERITY_ORDER.get(x["severity"], 9), x.get("scene", "")))

        # Apply truncation logic (mirrors _build_animation_audit)
        assert len(issues) > _MAX_ISSUES
        critical = [i for i in issues if i["severity"] == "critical"]
        warning = [i for i in issues if i["severity"] == "warning"]
        info = [i for i in issues if i["severity"] == "info"]

        kept: list[dict] = []
        for group in [critical, warning, info]:
            remaining = _MAX_ISSUES - len(kept)
            if remaining <= 0:
                break
            kept.extend(group[:remaining])

        assert len(kept) == _MAX_ISSUES
        # All 10 critical issues preserved
        assert sum(1 for i in kept if i["severity"] == "critical") == 10
        # Warning fills remaining slots
        assert sum(1 for i in kept if i["severity"] == "warning") == 30
        # Info gets the rest
        assert sum(1 for i in kept if i["severity"] == "info") == 10
