using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEditor;

/// <summary>
/// EerieVillage BT7-Plan 통합 EditMode 테스트 — Player 근거리 공격 체계·하트 분할 HP·자동 발동 타이머·아틀라스 참조.
/// Prefab/Script 자산의 컴포넌트·필드·YAML 구성이 기획 방향(BT7-Plan 2026-04-24)을 충족하는지 검증.
/// Play 모드 실행 불요 — prefab/script YAML 직렬화 상태를 직접 검증하여 회귀 방지.
///
/// 2026-04-23 BT5-Dev: reflection 기반 전환 (Scripts/ 하위 asmdef 부재 대응).
/// 2026-04-24 BT7-Plan: VS 순수형 자동 발동 전환에 맞춰 수동 입력(Attack 액션·attackCooldown) 검증 제거 +
///                       PlayerAttackTicker·Health.maxHearts/maxHP·IncreaseMaxHearts·Heal 필드 검증 신규.
/// </summary>
public class PlayerAttackTests
{
    const string PlayerPrefabPath = "Assets/Prefabs/Player.prefab";
    const string EnemyPrefabPath = "Assets/Prefabs/Enemy.prefab";

    const string AttackHitboxType = "Platformer.Mechanics.AttackHitbox";
    const string HealthType = "Platformer.Mechanics.Health";
    const string PlayerControllerType = "Platformer.Mechanics.PlayerController";
    const string EnemyControllerType = "Platformer.Mechanics.EnemyController";
    const string PlayerAttackTickerType = "Platformer.Gameplay.PlayerAttackTicker";

    static System.Type FindTypeByFullName(string fullName)
    {
        foreach (var asm in System.AppDomain.CurrentDomain.GetAssemblies())
        {
            var t = asm.GetType(fullName, throwOnError: false);
            if (t != null) return t;
        }
        return null;
    }

    static Component FindComponentByFullName(GameObject go, string fullName)
    {
        if (go == null) return null;
        return go.GetComponents<Component>()
            .FirstOrDefault(c => c != null && c.GetType().FullName == fullName);
    }

    static object GetFieldOrProperty(object obj, string memberName)
    {
        if (obj == null) return null;
        var t = obj.GetType();
        var field = t.GetField(memberName,
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        if (field != null) return field.GetValue(obj);
        var prop = t.GetProperty(memberName,
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        return prop?.GetValue(obj);
    }

    // ===== Player/Enemy Prefab 컴포넌트 구성 =====

    [Test]
    public void Player_Prefab_Has_AttackHitbox_Component()
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PlayerPrefabPath);
        Assert.IsNotNull(prefab, $"Player.prefab not found at {PlayerPrefabPath}");

        var hitbox = FindComponentByFullName(prefab, AttackHitboxType);
        Assert.IsNotNull(hitbox,
            "Player.prefab에 AttackHitbox 컴포넌트가 누락. " +
            "BT5-Dev 2단계 재위임 집행분 (2026-04-23) 이 prefab YAML 에 반영되어야 함.");
    }

    [Test]
    public void Player_Prefab_Has_Health_Component()
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PlayerPrefabPath);
        Assert.IsNotNull(prefab, $"Player.prefab not found at {PlayerPrefabPath}");

        var health = FindComponentByFullName(prefab, HealthType);
        Assert.IsNotNull(health, "Player.prefab 에 Health 컴포넌트 누락 (템플릿 기본).");
    }

    [Test]
    public void Player_Prefab_Has_PlayerController_Component()
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PlayerPrefabPath);
        Assert.IsNotNull(prefab);

        var controller = FindComponentByFullName(prefab, PlayerControllerType);
        Assert.IsNotNull(controller, "Player.prefab 에 PlayerController 컴포넌트 누락.");
    }

    [Test]
    public void AttackHitbox_Default_Damage_Is_One()
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PlayerPrefabPath);
        var hitbox = FindComponentByFullName(prefab, AttackHitboxType);
        Assert.IsNotNull(hitbox);

        var damage = GetFieldOrProperty(hitbox, "damage");
        Assert.IsNotNull(damage, "AttackHitbox.damage 필드/프로퍼티 접근 불가");
        Assert.AreEqual(1, System.Convert.ToInt32(damage),
            "기본 대미지 1 (Phase 3-B balance/01 v0.2 튠 전 파일럿 값).");
    }

    [Test]
    public void AttackHitbox_Active_Duration_Is_Positive()
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PlayerPrefabPath);
        var hitbox = FindComponentByFullName(prefab, AttackHitboxType);
        Assert.IsNotNull(hitbox);

        var duration = GetFieldOrProperty(hitbox, "activeDuration");
        Assert.IsNotNull(duration, "AttackHitbox.activeDuration 필드/프로퍼티 접근 불가");
        Assert.Greater(System.Convert.ToSingle(duration), 0f,
            "activeDuration 가 0 이하면 OverlapBox 판정이 즉시 종료되어 공격 무효.");
    }

    [Test]
    public void Enemy_Prefab_Has_Health_Component()
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(EnemyPrefabPath);
        Assert.IsNotNull(prefab, $"Enemy.prefab not found at {EnemyPrefabPath}");

        var health = FindComponentByFullName(prefab, HealthType);
        Assert.IsNotNull(health,
            "Enemy.prefab 에 Health 컴포넌트 누락. " +
            "Health 없으면 AttackHitbox.Update 의 Decrement 호출이 불가 → EnemyDeath 체인 미발동.");
    }

    [Test]
    public void Enemy_Prefab_Has_EnemyController_Component()
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(EnemyPrefabPath);
        var controller = FindComponentByFullName(prefab, EnemyControllerType);
        Assert.IsNotNull(controller,
            "EnemyDeath 체인에 EnemyController 필수 (AttackHitbox.Update 에서 Schedule<EnemyDeath>().enemy = enemy).");
    }

    // ===== BT7-Plan 하트 분할 시스템 검증 (2026-04-24) =====

    [Test]
    public void Health_Script_Defines_MaxHearts_And_IncreaseMaxHearts()
    {
        // PlayerAttack.Execute 등에서 Health 타입을 참조할 때의 계약. 필드·메서드 모두 존재해야 함.
        var healthType = FindTypeByFullName(HealthType);
        Assert.IsNotNull(healthType, "Platformer.Mechanics.Health 타입 로드 실패.");

        var maxHeartsField = healthType.GetField("maxHearts",
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        Assert.IsNotNull(maxHeartsField, "Health.maxHearts 필드 부재 (BT7-Plan 하트 분할 전제).");

        var maxHPField = healthType.GetField("maxHP",
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        Assert.IsNotNull(maxHPField, "Health.maxHP 필드 부재 (기존 API 호환 유지 필요).");

        var increaseMethod = healthType.GetMethod("IncreaseMaxHearts",
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        Assert.IsNotNull(increaseMethod, "Health.IncreaseMaxHearts(int) 메서드 부재 — 패시브 카드 훅 필수.");

        var healMethod = healthType.GetMethod("Heal",
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        Assert.IsNotNull(healMethod, "Health.Heal(int) 메서드 부재 — 쿼터 단위 회복 필수.");
    }

    [Test]
    public void Player_Prefab_MaxHP_Reflects_Heart_Quarters()
    {
        // Player.prefab의 Health 직렬화 값: maxHearts와 maxHP의 정합성 확인.
        // maxHP는 maxHearts * 4 또는 0(Awake 재계산 신규 프리팹). 런타임 Awake에서 재산정되지만
        // Inspector 기본값이 "1"인 구식 프리팹이 섞여 있으면 감지 필요.
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PlayerPrefabPath);
        var health = FindComponentByFullName(prefab, HealthType);
        Assert.IsNotNull(health);

        var maxHearts = System.Convert.ToInt32(GetFieldOrProperty(health, "maxHearts"));
        var maxHP = System.Convert.ToInt32(GetFieldOrProperty(health, "maxHP"));
        Assert.GreaterOrEqual(maxHearts, 1, "Player 초기 maxHearts는 1 이상 (BT7-Plan 기본 하트 1개).");
        Assert.IsTrue(maxHP == maxHearts * 4 || maxHP == 1,
            $"Player.prefab Health.maxHP({maxHP})는 maxHearts*4({maxHearts * 4}) 또는 구식 기본값 1이어야 함. " +
            "Awake에서 재산정되나 prefab 직렬화 값 점검으로 회귀 방지.");
    }

    // ===== BT7-Plan 자동 발동 타이머 검증 (2026-04-24) =====

    [Test]
    public void PlayerAttackTicker_Script_Exists_With_AttackInterval()
    {
        // PlayerAttackTicker가 존재하고 attackInterval 필드를 노출하는지 검증.
        // 실제 Player.prefab 부착 여부는 Play 모드 검증 외에 YAML로도 확인 가능하나,
        // 컴포넌트 첨부가 Unity 수동 작업 or 향후 prefab 편집으로 분리되어도 Script 계약만큼은 유지되어야 함.
        var tickerType = FindTypeByFullName(PlayerAttackTickerType);
        Assert.IsNotNull(tickerType,
            "Platformer.Gameplay.PlayerAttackTicker 타입 로드 실패. " +
            "BT7-Plan VS 순수형 자동 발동 전환 전제.");

        var intervalField = tickerType.GetField("attackInterval",
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        Assert.IsNotNull(intervalField, "PlayerAttackTicker.attackInterval 필드 부재.");
        Assert.AreEqual(typeof(float), intervalField.FieldType,
            "attackInterval은 float이어야 함 (Time.deltaTime 누적용).");
    }

    // ===== BT7-Plan 공격 버튼 제거 검증 =====

    [Test]
    public void InputActions_Player_Map_Has_No_Attack_Action()
    {
        // InputSystem_Actions.inputactions의 Player 맵에 Attack 액션이 완전히 제거되었는지 YAML 검증.
        var path = System.IO.Path.GetFullPath("Assets/Settings/InputSystem_Actions.inputactions");
        Assert.IsTrue(System.IO.File.Exists(path), $"InputActions 파일 부재 — {path}");
        var json = System.IO.File.ReadAllText(path);

        // Player 맵 내부에 "Attack" 이름의 액션이 없어야 함. UI 맵 범위는 무시.
        var playerMapStart = json.IndexOf("\"name\": \"Player\"", System.StringComparison.Ordinal);
        Assert.Greater(playerMapStart, 0, "Player 맵 식별 실패.");
        var uiMapStart = json.IndexOf("\"name\": \"UI\"", playerMapStart, System.StringComparison.Ordinal);
        Assert.Greater(uiMapStart, playerMapStart, "UI 맵 식별 실패.");

        var playerMapSection = json.Substring(playerMapStart, uiMapStart - playerMapStart);
        Assert.IsFalse(
            playerMapSection.Contains("\"name\": \"Attack\""),
            "Player 맵에 Attack 액션 잔존. BT7-Plan VS 순수형 전환으로 제거되어야 함.");
    }

    // ===== BT5-Dev 3단계 PlayerTestGirl 아틀라스 적용 검증 (2026-04-24 계승) =====

    const string PlayerTestGirlGuid = "44ad58ba82191ca4d818108ab01d3baa";

    [Test]
    public void Player_Prefab_SpriteRenderer_References_PlayerTestGirl()
    {
        var path = System.IO.Path.GetFullPath(PlayerPrefabPath);
        Assert.IsTrue(System.IO.File.Exists(path), $"Player.prefab 부재 — {path}");
        var yaml = System.IO.File.ReadAllText(path);

        var match = System.Text.RegularExpressions.Regex.Match(
            yaml,
            @"m_Sprite:\s*\{fileID:\s*[-0-9]+,\s*guid:\s*([a-f0-9]{32}),\s*type:\s*3\}");
        Assert.IsTrue(match.Success,
            "Player.prefab YAML 에서 SpriteRenderer.m_Sprite guid 추출 실패.");
        Assert.AreEqual(PlayerTestGirlGuid, match.Groups[1].Value,
            $"Player.prefab SpriteRenderer.m_Sprite guid 가 PlayerTestGirl({PlayerTestGirlGuid}) 이어야 함. 실제: {match.Groups[1].Value}");
    }

    [Test]
    public void Player_Controller_Has_Attack_Parameter_And_State()
    {
        var path = System.IO.Path.GetFullPath("Assets/Character/Animations/Player.controller");
        Assert.IsTrue(System.IO.File.Exists(path), $"Player.controller 부재 — {path}");
        var yaml = System.IO.File.ReadAllText(path);

        Assert.IsTrue(
            System.Text.RegularExpressions.Regex.IsMatch(
                yaml,
                @"m_Name:\s*attack\s*\r?\n\s*m_Type:\s*9"),
            "Player.controller 에 attack Trigger 파라미터(m_Type:9) 누락. " +
            "자동 발동 PlayerAttack → Animator.SetTrigger 연동 전제.");

        Assert.IsTrue(
            yaml.Contains("m_Name: Player-Attack"),
            "Player.controller 에 Player-Attack State 누락. " +
            "PlayerAttack.anim(guid c8d7e5a1f9b24e63a7f5d2c8e1b9a4f7) 모션을 호스트하는 State 가 필요.");
    }
}
