"""
Unit tests for Savage Weapons & Martial Sects System in FreeExile.
Verifies:
1. Weapon catalog loading & coverage across 5 Sects and 5 Primal Clans.
2. Attribute requirements validation (Physique, Agility, Mind).
3. Base damage scaling with Character Attributes.
4. Dual Wielding mechanics (+10% More APS, +15% Block Chance).
5. Two-Handed weapon potency (+50% More Base Damage, extended reach).
6. Shield equipping and Block Chance computation.
7. Sect & Clan Affinity Resonance Bonus.
8. Zero-latency (0ms) Dual Weapon Auto-Swap for skill execution.
9. Weapon implicit affixes application.
"""

import unittest
import sys
import os

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../server")))

from server.world.martial_types import FiveElements, MartialSect, WeaponCategory
from server.world.weapon_types import (
    WeaponClass,
    GripStyle,
    PrimalClan,
    WeaponImplicitStat,
    WeaponImplicitMod,
    BaseWeaponTemplate,
    EquippedWeapon,
    WeaponSetLoadout,
    WeaponScalingResult,
    DualWieldBonus,
)
from server.world.weapon_catalog import get_base_weapon_catalog, get_weapons_by_sect, get_weapons_by_clan
from server.world.weapon_engine import WeaponEngine


class TestWeaponSystem(unittest.TestCase):
    def setUp(self):
        self.catalog = get_base_weapon_catalog()
        self.engine = WeaponEngine()

    def test_catalog_coverage_and_uniqueness(self):
        """Catalog must contain rich weapons covering all 5 sects and 5 clans."""
        self.assertGreaterEqual(len(self.catalog), 20)
        
        # Verify all sects have at least 3 dedicated weapons
        for sect in MartialSect:
            sect_weapons = get_weapons_by_sect(sect)
            self.assertGreaterEqual(len(sect_weapons), 3, f"Sect {sect} should have >= 3 weapons")

        # Verify all clans have at least 2 dedicated weapons
        for clan in PrimalClan:
            clan_weapons = get_weapons_by_clan(clan)
            self.assertGreaterEqual(len(clan_weapons), 2, f"Clan {clan} should have >= 2 weapons")

    def test_attribute_requirements_check(self):
        """Should validate if character meets weapon attribute thresholds."""
        # Greatsword requires high Physique
        blade_wpn = self.catalog["WPN_BLADE_2H_XICH_HUYET"]
        
        # Low physique char -> Fail
        low_stats = {"cuong_the": 20, "than_phap": 50, "than_niem": 20}
        self.assertFalse(self.engine.validate_requirements(blade_wpn, low_stats))

        # Qualified char -> Pass
        qualified_stats = {"cuong_the": 120, "than_phap": 50, "than_niem": 20}
        self.assertTrue(self.engine.validate_requirements(blade_wpn, qualified_stats))

    def test_damage_scaling_with_attributes(self):
        """Attributes must scale physical and elemental damage accordingly."""
        sword = self.catalog["WPN_SWORD_1H_COT_KIEM"]
        equipped = EquippedWeapon(template=sword, quality=20)

        # Baseline stats
        base_stats = {"cuong_the": 50, "than_phap": 50, "than_niem": 50}
        base_res = self.engine.calculate_weapon_damage(equipped, base_stats, GripStyle.ONE_HANDED)

        # High Agility stats (Sword scales primarily with Dex/Agility)
        high_agi_stats = {"cuong_the": 50, "than_phap": 150, "than_niem": 50}
        high_res = self.engine.calculate_weapon_damage(equipped, high_agi_stats, GripStyle.ONE_HANDED)

        self.assertGreater(high_res.effective_dps, base_res.effective_dps)
        self.assertGreater(high_res.final_crit_chance, base_res.final_crit_chance)

    def test_dual_wielding_mechanic(self):
        """Dual wielding granting +10% More Attack Speed and +15% Block Chance."""
        sword_main = EquippedWeapon(template=self.catalog["WPN_SWORD_1H_COT_KIEM"])
        dagger_off = EquippedWeapon(template=self.catalog["WPN_SWORD_1H_THANH_PHONG"])
        stats = {"cuong_the": 60, "than_phap": 80, "than_niem": 40}

        # Single 1H
        single_res = self.engine.calculate_weapon_damage(sword_main, stats, GripStyle.ONE_HANDED)
        
        # Dual Wield
        dual_res = self.engine.calculate_weapon_damage(
            sword_main, stats, GripStyle.ONE_HANDED, offhand_weapon=dagger_off
        )

        self.assertAlmostEqual(dual_res.effective_attack_speed, single_res.effective_attack_speed * 1.10, places=2)
        self.assertGreaterEqual(dual_res.bonus_block_chance, 0.15)

    def test_two_handed_heavy_potency(self):
        """Two-Handed weapon grants +50% More Base Damage and wider reach."""
        club_2h = EquippedWeapon(template=self.catalog["WPN_CLUB_2H_HUYEN_THIET"])
        stats = {"cuong_the": 100, "than_phap": 40, "than_niem": 30}

        res_2h = self.engine.calculate_weapon_damage(club_2h, stats, GripStyle.TWO_HANDED)
        self.assertTrue(res_2h.is_two_handed)
        self.assertGreaterEqual(res_2h.weapon_reach_meters, 2.0)
        self.assertGreaterEqual(res_2h.stun_potency_multiplier, 1.3)

    def test_shield_equipping_and_block(self):
        """Shield in offhand must grant block chance and armor/ward barrier."""
        main_weapon = EquippedWeapon(template=self.catalog["WPN_SWORD_1H_COT_KIEM"])
        shield = EquippedWeapon(template=self.catalog["WPN_SHIELD_HAC_BI_THUAN"])
        stats = {"cuong_the": 80, "than_phap": 50, "than_niem": 30}

        loadout = WeaponSetLoadout(mainhand=main_weapon, offhand=shield)
        eval_res = self.engine.evaluate_loadout(loadout, stats)

        self.assertTrue(eval_res.has_shield)
        self.assertGreaterEqual(eval_res.total_block_chance, 0.25)
        self.assertGreater(eval_res.bonus_armor, 0)

    def test_sect_and_clan_affinity_resonance(self):
        """Matching Sect and Clan grants thematic bonus damage and stats."""
        sword = self.catalog["WPN_SWORD_1H_THANH_PHONG"] # Sect: THANH_PHONG, Clan: GALE_PHANTOM
        equipped = EquippedWeapon(template=sword)
        
        # Non-matching
        no_res = self.engine.get_affinity_resonance(
            equipped, char_sect=MartialSect.KIM_CANG, char_clan=PrimalClan.CRIMSON_BLOOD
        )
        self.assertEqual(no_res.total_damage_multiplier, 1.0)
        self.assertFalse(no_res.has_sect_resonance)
        self.assertFalse(no_res.has_clan_resonance)

        # Matching Sect & Clan
        full_res = self.engine.get_affinity_resonance(
            equipped, char_sect=MartialSect.THANH_PHONG, char_clan=PrimalClan.GALE_PHANTOM
        )
        self.assertTrue(full_res.has_sect_resonance)
        self.assertTrue(full_res.has_clan_resonance)
        self.assertGreater(full_res.total_damage_multiplier, 1.15)

    def test_zero_latency_auto_swap(self):
        """Auto-swap finds matching weapon set instantly in 0ms."""
        sword_set = WeaponSetLoadout(
            mainhand=EquippedWeapon(template=self.catalog["WPN_SWORD_1H_COT_KIEM"])
        )
        bow_set = WeaponSetLoadout(
            mainhand=EquippedWeapon(template=self.catalog["WPN_BOW_2H_COT_TIEN"])
        )

        # Currently active set is 1 (Sword). Skill requires PROJECTILE_WEAPON.
        swap_result = self.engine.resolve_auto_swap(
            current_active_set=1,
            weapon_set_1=sword_set,
            weapon_set_2=bow_set,
            required_category=WeaponCategory.PROJECTILE_WEAPON
        )

        self.assertTrue(swap_result.swapped)
        self.assertEqual(swap_result.new_active_set, 2)
        self.assertEqual(swap_result.selected_weapon.template.weapon_class, WeaponClass.RANGED_BOW)

        # Skill requires SWORD -> swaps back to set 1
        swap_back = self.engine.resolve_auto_swap(
            current_active_set=2,
            weapon_set_1=sword_set,
            weapon_set_2=bow_set,
            required_category=WeaponCategory.SWORD
        )
        self.assertTrue(swap_back.swapped)
        self.assertEqual(swap_back.new_active_set, 1)

    def test_weapon_implicit_affix_applied(self):
        """Each weapon base template must carry an appropriate implicit affix."""
        bow = self.catalog["WPN_BOW_2H_COT_TIEN"]
        self.assertIsNotNone(bow.implicit_mod)
        self.assertEqual(bow.implicit_mod.stat, WeaponImplicitStat.ADDITIONAL_PIERCE)
        self.assertGreaterEqual(bow.implicit_mod.value, 1.0)


if __name__ == "__main__":
    unittest.main()
