"""
Unit test suite for Expanded Server-Authoritative Meridian Passive Tree.
Verifies 29-node multi-cluster constellation, Minor/Notable/Keystone hierarchy,
Linh Thạch (Jewel) socketing/unsocketing, anti-cheat pathing, and persistence.
"""

import os
import tempfile
import unittest

from server.world.combat_engine import CombatActor, FiveElements
from server.world.level_progression_types import LevelUpEvent
from server.world.meridian_catalog import (
    CANONICAL_JEWELS,
    CANONICAL_MERIDIAN_NODES,
    get_all_canonical_jewels,
    get_all_meridian_nodes,
    get_canonical_jewel,
    get_meridian_node,
    get_node_neighbors,
)
from server.world.meridian_service import MeridianService
from server.world.meridian_types import MeridianNodeType
from server.world.quest_engine import QuestEngine


class TestMeridianServerService(unittest.TestCase):
    def setUp(self) -> None:
        self.temp_dir = tempfile.TemporaryDirectory()
        self.db_path = os.path.join(self.temp_dir.name, "test_game_state.db")
        self.quest_engine = QuestEngine()
        self.service = MeridianService(
            db_path=self.db_path,
            quest_engine=self.quest_engine,
        )
        self.player_id = "test_player_feral_01"

    def tearDown(self) -> None:
        try:
            self.temp_dir.cleanup()
        except Exception:
            pass

    def test_expanded_catalog_integrity(self) -> None:
        """Verifies 29 nodes across 5 clusters, with Minor, Notable, Keystones, and Jewel Sockets."""
        nodes = get_all_meridian_nodes()
        self.assertEqual(len(nodes), 29)

        # Check clusters exist
        clusters = {n.cluster for n in nodes.values()}
        self.assertEqual(clusters, {"CENTER", "NORTH", "EAST", "SOUTH", "WEST"})

        # Check node types
        types = {n.node_type for n in nodes.values()}
        self.assertIn(MeridianNodeType.ORIGIN, types)
        self.assertIn(MeridianNodeType.VESSEL, types)
        self.assertIn(MeridianNodeType.MAJOR, types)
        self.assertIn(MeridianNodeType.KEYSTONE, types)
        self.assertIn(MeridianNodeType.JEWEL_SOCKET, types)

        # Verify 4 Keystones & 4 Jewel Sockets
        keystones = [n for n in nodes.values() if n.node_type == MeridianNodeType.KEYSTONE]
        self.assertEqual(len(keystones), 4)

        sockets = [n for n in nodes.values() if n.node_type == MeridianNodeType.JEWEL_SOCKET]
        self.assertEqual(len(sockets), 4)

        # Verify Canonical Jewels
        jewels = get_all_canonical_jewels()
        self.assertEqual(len(jewels), 4)
        self.assertIn("jewel_red_bloodstone", jewels)
        self.assertIn("jewel_gold_thunder", jewels)

    def test_initial_authoritative_points(self) -> None:
        """Every new character begins with authoritative 5 points on the server."""
        state = self.service.get_or_create_player(self.player_id)
        self.assertEqual(state.available_points, 5)
        self.assertEqual(state.spent_points, 0)
        self.assertEqual(len(state.unlocked_nodes), 0)
        self.assertEqual(len(state.socketed_jewels), 0)

    def test_topological_anti_cheat_gating(self) -> None:
        """Rejects unlocking distant clusters or keystones without contiguous path."""
        # Attempt to unlock keystone m_n_keystone directly
        can_unlock, reason = self.service.can_unlock_node(self.player_id, "m_n_keystone")
        self.assertFalse(can_unlock)
        self.assertIn("kế cận", reason)

        res = self.service.unlock_node(self.player_id, "m_n_keystone")
        self.assertFalse(res.success)
        self.assertEqual(res.available_points, 5)

    def test_sequential_path_unlocking(self) -> None:
        """Validates pathing: Center m_c1 -> m_c3 -> North m_n1 -> m_n3."""
        # 1. Unlock Origin m_c1
        r1 = self.service.unlock_node(self.player_id, "m_c1")
        self.assertTrue(r1.success)
        self.assertEqual(r1.available_points, 4)
        self.assertEqual(r1.aggregated_stats.hp, 200)

        # 2. Unlock Center node m_c3
        r2 = self.service.unlock_node(self.player_id, "m_c3")
        self.assertTrue(r2.success)
        self.assertEqual(r2.available_points, 3)

        # 3. Unlock North gate m_n1
        r3 = self.service.unlock_node(self.player_id, "m_n1")
        self.assertTrue(r3.success)
        self.assertEqual(r3.available_points, 2)

        # 4. Unlock North Notable m_n3
        r4 = self.service.unlock_node(self.player_id, "m_n3")
        self.assertTrue(r4.success)
        self.assertEqual(r4.available_points, 1)
        self.assertIn("m_n3", r4.unlocked_nodes)
        self.assertGreater(r4.aggregated_stats.dps_mult, 0.20)

    def test_jewel_socketing_and_unsocketing(self) -> None:
        """Verifies full lifecycle of Jewel Sockets (Linh Cốt Khảm Tọa)."""
        # Attempt socketing into locked socket should fail
        sock_fail = self.service.socket_jewel(self.player_id, "m_n_jewel", "jewel_red_bloodstone")
        self.assertFalse(sock_fail.success)
        self.assertIn("chưa được khai thông", sock_fail.message)

        # Unlock path to m_n_jewel: m_c1 -> m_c3 -> m_n1 -> m_n2 -> m_n_jewel (5 nodes)
        path = ["m_c1", "m_c3", "m_n1", "m_n2", "m_n_jewel"]
        for nid in path:
            r = self.service.unlock_node(self.player_id, nid)
            self.assertTrue(r.success, f"Failed on {nid}")

        stats_before_jewel = self.service.compute_total_stats(self.player_id)

        # Socket "jewel_red_bloodstone" (+120 HP, +12% melee dmg, +15 armor)
        sock_res = self.service.socket_jewel(self.player_id, "m_n_jewel", "jewel_red_bloodstone")
        self.assertTrue(sock_res.success)
        self.assertIn("Khảm nạp thành công", sock_res.message)

        stats_with_jewel = self.service.compute_total_stats(self.player_id)
        self.assertEqual(stats_with_jewel.hp, stats_before_jewel.hp + 120)
        self.assertEqual(stats_with_jewel.armor, stats_before_jewel.armor + 15)
        self.assertAlmostEqual(stats_with_jewel.dps_mult, stats_before_jewel.dps_mult + 0.12, places=2)

        # Unsocket jewel
        unsock_res = self.service.unsocket_jewel(self.player_id, "m_n_jewel")
        self.assertTrue(unsock_res.success)
        self.assertEqual(unsock_res.removed_jewel_id, "jewel_red_bloodstone")

        stats_after_unsocket = self.service.compute_total_stats(self.player_id)
        self.assertEqual(stats_after_unsocket.hp, stats_before_jewel.hp)
        self.assertEqual(stats_after_unsocket.armor, stats_before_jewel.armor)

    def test_reset_refunds_points_and_clears_jewels(self) -> None:
        """Reset refunds spent points, clears nodes, and unsockets jewels."""
        # Unlock m_c1 -> m_c3 -> m_n1
        for nid in ["m_c1", "m_c3", "m_n1"]:
            self.service.unlock_node(self.player_id, nid)

        reset_res = self.service.reset_meridian(self.player_id)
        self.assertTrue(reset_res.success)
        self.assertEqual(reset_res.refunded_points, 3)
        self.assertEqual(reset_res.available_points, 5)

        state = self.service.get_or_create_player(self.player_id)
        self.assertEqual(len(state.unlocked_nodes), 0)
        self.assertEqual(len(state.socketed_jewels), 0)

    def test_sqlite_persistence_with_jewels(self) -> None:
        """State including unlocked nodes and socketed jewels persists across DB reloads."""
        # Unlock path to m_n_jewel and socket a jewel
        path = ["m_c1", "m_c3", "m_n1", "m_n2", "m_n_jewel"]
        for nid in path:
            self.service.unlock_node(self.player_id, nid)
        self.service.socket_jewel(self.player_id, "m_n_jewel", "jewel_gold_thunder")

        # Reload new service instance
        new_service = MeridianService(db_path=self.db_path)
        loaded_state = new_service.get_or_create_player(self.player_id)

        self.assertEqual(loaded_state.available_points, 0)
        self.assertEqual(loaded_state.spent_points, 5)
        self.assertEqual(len(loaded_state.unlocked_nodes), 5)
        self.assertIn("m_n_jewel", loaded_state.socketed_jewels)
        self.assertEqual(loaded_state.socketed_jewels["m_n_jewel"].jewel_id, "jewel_gold_thunder")

        # Total stats must include jewel stats (+12% crit rate, +35% crit dmg)
        total_stats = new_service.compute_total_stats(self.player_id)
        self.assertGreaterEqual(total_stats.crit_rate, 0.12)
        self.assertGreaterEqual(total_stats.crit_dmg, 0.35)


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