"""
Sprint 5 TDD Unit & Integration Test Suite for FreeExile:
KIỂM THỬ TẢI TRIỆU CCU & XUẤT XƯỞNG BẢN GOLD CHO IOS (TUẦN 8)
- 1. Thử nghiệm chịu tải 1M CCU Swarm Simulation (Headless Clients Swarm)
- 2. Profiling Hiệu Năng iOS Metal & Thiết Bị Di Động (Metal System Trace / 120 FPS Benchmark)
- 3. Đóng Gói Xuất Xưởng Bản Gold Master iOS (TestFlight / IPA Packaging & Kubernetes Ops)
- 4. Kiểm toán Timestamp Hoàn Thành Roadmap & Release Report (Sprint 1-5 ISO-8601 Timestamps)
"""

from __future__ import annotations

import os
import plistlib
import re
import sys
import unittest
import yaml

PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
SERVER_DIR = os.path.join(PROJECT_ROOT, "server")
if SERVER_DIR not in sys.path:
    sys.path.insert(0, SERVER_DIR)
if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)

from server.world.swarm_load_harness import (
    SwarmConfig,
    SwarmSimulationHarness,
    SwarmBenchmarkResult,
    VirtualActorBatch,
)
from tools.perf.ios_metal_profiler import (
    IOSMetalProfilerConfig,
    IOSMetalPerformanceProfiler,
    MetalBenchmarkReport,
    ZeroAllocScratchMemory,
)


class TestSprint5SwarmSimulation1MCCU(unittest.TestCase):
    """Test suite for 1,000,000 CCU Swarm Simulation Load Harness."""

    def setUp(self) -> None:
        self.config = SwarmConfig(
            total_simulated_ccu=1_000_000,
            shards_count=100,
            spatial_cell_size=64.0,
            target_tick_rate_hz=30,
            target_p99_latency_ms=25.0,
            max_ram_per_node_gb=2.5,
            sample_active_actors=2_000,
        )
        self.harness = SwarmSimulationHarness(self.config)

    def test_swarm_config_validation(self) -> None:
        """Validates 1M CCU cluster architecture parameters."""
        self.assertEqual(self.config.total_simulated_ccu, 1_000_000)
        self.assertEqual(self.config.shards_count, 100)
        self.assertEqual(self.config.actors_per_shard, 10_000)
        self.assertEqual(self.config.target_p99_latency_ms, 25.0)
        self.assertEqual(self.config.max_ram_per_node_gb, 2.5)

    def test_spatial_cell_batching_and_actor_partitioning(self) -> None:
        """Verifies spatial cell distribution across virtual headless actors."""
        batch = VirtualActorBatch(shard_id=1, actor_count=500, cell_size=64.0)
        batch.initialize_actors()
        self.assertEqual(len(batch.actors), 500)
        cell_distribution = batch.get_cell_distribution()
        self.assertGreater(len(cell_distribution), 1)

        # Tick spatial displacement
        tick_duration_ms = batch.step_simulation_tick(dt=0.033)
        self.assertGreater(tick_duration_ms, 0.0)
        self.assertLess(tick_duration_ms, 15.0)

    def test_spatial_cell_cleanup_and_query_nearby(self) -> None:
        """Verifies dead empty cells are pruned and nearby AOI querying works."""
        batch = VirtualActorBatch(shard_id=1, actor_count=20, cell_size=64.0)
        batch.initialize_actors()
        # Move all actors by a large offset to vacate initial cells
        for actor in batch.actors:
            actor.vx = 500.0
            actor.vy = 500.0
        batch.step_simulation_tick(dt=1.0)
        # Verify no empty sets remain in cells map
        for coords, actor_ids in batch.cells.items():
            self.assertGreater(len(actor_ids), 0, f"Empty set left in cells at {coords}")
        nearby = batch.get_nearby_actors(cx=batch.actors[0].cell_x, cy=batch.actors[0].cell_y)
        self.assertIn(batch.actors[0].actor_id, nearby)

    def test_swarm_simulation_zero_ticks_edge_case(self) -> None:
        """Verifies harness handles 0 sample ticks gracefully without ZeroDivisionError."""
        result = self.harness.run_benchmark(sample_ticks=0)
        self.assertFalse(result.is_passing)
        self.assertEqual(result.total_packets_processed, 0)
        self.assertEqual(result.spatial_grid_tick_ms, 0.0)

    def test_swarm_simulation_execution_and_metrics(self) -> None:
        """Runs the load harness and validates p99 latency, tick time and RAM."""
        result: SwarmBenchmarkResult = self.harness.run_benchmark(sample_ticks=10)
        self.assertEqual(result.simulated_ccu, 1_000_000)
        self.assertEqual(result.active_shards, 100)
        self.assertTrue(result.is_passing, f"Benchmark failed: {result}")
        self.assertLess(result.spatial_grid_tick_ms, 10.0)
        self.assertLess(result.p99_latency_ms, 25.0)
        self.assertLess(result.ram_per_node_gb, 2.5)
        self.assertEqual(result.packet_loss_rate, 0.0)
        self.assertGreater(result.throughput_packets_per_sec, 10_000)

    def test_distributed_node_memory_scaling(self) -> None:
        """Verifies that individual node memory stays well within the 2.5GB limit."""
        projected_ram = self.harness.estimate_shard_ram_gb(actors_count=10_000)
        self.assertLess(projected_ram, 2.5)
        self.assertGreater(projected_ram, 0.05)


class TestSprint5IOSMetalPerformanceProfiling(unittest.TestCase):
    """Test suite for iOS Metal 120 FPS ProMotion & Zero-Allocation Profiling."""

    def setUp(self) -> None:
        self.config = IOSMetalProfilerConfig(
            target_fps=120,
            frame_budget_ms=8.33,
            max_ram_budget_mb=450.0,
            max_thermal_temp_celsius=38.0,
            sample_frames=120,
        )
        self.profiler = IOSMetalPerformanceProfiler(self.config)

    def test_metal_profiler_config_defaults(self) -> None:
        """Verifies iOS 120Hz ProMotion constraints."""
        self.assertEqual(self.config.target_fps, 120)
        self.assertAlmostEqual(self.config.frame_budget_ms, 8.33, delta=0.01)
        self.assertEqual(self.config.max_ram_budget_mb, 450.0)
        self.assertEqual(self.config.max_thermal_temp_celsius, 38.0)

    def test_frame_budget_audit_120fps_promotion(self) -> None:
        """Audits frame times to guarantee 120 FPS ProMotion (< 8.33ms)."""
        report: MetalBenchmarkReport = self.profiler.run_benchmark()
        self.assertTrue(report.is_promotion_compliant)
        self.assertLess(report.avg_frame_time_ms, 8.33)
        self.assertLess(report.p99_frame_time_ms, 8.33)
        self.assertGreaterEqual(report.avg_fps, 120.0)

    def test_zero_allocation_hot_paths_in_render_kinematics(self) -> None:
        """Verifies zero allocations occurred during render & kinematics hot path."""
        report: MetalBenchmarkReport = self.profiler.run_benchmark()
        self.assertTrue(report.zero_alloc_verified)
        self.assertEqual(report.hot_path_allocations, 0)

    def test_metal_profiler_detects_leaky_hot_path_negative_test(self) -> None:
        """Negative baseline: ensures profiler correctly catches dynamic allocations in hot path."""
        class LeakyProfiler(IOSMetalPerformanceProfiler):
            def __init__(self, cfg: IOSMetalProfilerConfig) -> None:
                super().__init__(cfg)
                self.leaks: list[object] = []

            def _execute_zero_alloc_hot_loop(self, dt: float) -> None:
                super()._execute_zero_alloc_hot_loop(dt)
                self.leaks.append([dt, dt * 2.0])

        leaky_profiler = LeakyProfiler(IOSMetalProfilerConfig(sample_frames=10))
        report = leaky_profiler.run_benchmark()
        self.assertFalse(report.zero_alloc_verified)
        self.assertGreater(report.hot_path_allocations, 0)
        self.assertFalse(report.is_promotion_compliant)

    def test_scratch_memory_acquire_and_bounds_check(self) -> None:
        """Tests zero-alloc scratch memory slicing and buffer capacity safety."""
        scratch = ZeroAllocScratchMemory(capacity=16)
        slice1 = scratch.acquire_scratch_slice(32)
        self.assertEqual(len(slice1), 32)
        self.assertEqual(scratch.cursor, 32)
        scratch.reset()
        self.assertEqual(scratch.cursor, 0)
        with self.assertRaises(BufferError):
            scratch.acquire_scratch_slice(16 * 16 + 1)

    def test_mobile_ram_budget_under_450mb(self) -> None:
        """Verifies mobile memory footprint is strictly under 450MB."""
        report: MetalBenchmarkReport = self.profiler.run_benchmark()
        self.assertLess(report.peak_ram_mb, 450.0)

    def test_thermal_stability_under_38_degrees(self) -> None:
        """Verifies thermal simulation remains <= 38.0°C."""
        report: MetalBenchmarkReport = self.profiler.run_benchmark()
        self.assertLessEqual(report.steady_temp_celsius, 38.0)


class TestSprint5IOSPackagingAndKubernetesOps(unittest.TestCase):
    """Test suite for iOS Packaging, Entitlements and Kubernetes Ops."""

    def test_info_plist_promotion_120hz_and_metal_configured(self) -> None:
        """Verifies Info.plist enables 120Hz ProMotion and Metal API capabilities."""
        info_plist_path = os.path.join(PROJECT_ROOT, "platform/ios/FreeExile/Info.plist")
        self.assertTrue(os.path.exists(info_plist_path), "Info.plist does not exist")

        with open(info_plist_path, "rb") as f:
            plist_data = plistlib.load(f)

        self.assertEqual(plist_data.get("CFBundleIdentifier"), "com.freeexile.arpg")
        self.assertTrue(plist_data.get("CADisableMinimumFrameDurationOnPhone"), "Missing CADisableMinimumFrameDurationOnPhone for 120Hz ProMotion")
        req_caps = plist_data.get("UIRequiredDeviceCapabilities", [])
        self.assertIn("arm64", req_caps)
        self.assertIn("metal", req_caps)

    def test_entitlements_app_attest_game_center_secure_enclave(self) -> None:
        """Verifies FreeExile.entitlements contains App Attest, Game Center, Secure Enclave."""
        entitlements_path = os.path.join(PROJECT_ROOT, "platform/ios/FreeExile/FreeExile.entitlements")
        self.assertTrue(os.path.exists(entitlements_path), "FreeExile.entitlements does not exist")

        with open(entitlements_path, "rb") as f:
            entitlements = plistlib.load(f)

        self.assertIn("com.apple.developer.devicecheck.appattest-environment", entitlements)
        self.assertTrue(entitlements.get("com.apple.developer.game-center"))
        self.assertIn("keychain-access-groups", entitlements)

    def test_export_plist_testflight_ready(self) -> None:
        """Verifies ios_testflight_export.plist is configured for App Store Connect."""
        export_plist_path = os.path.join(PROJECT_ROOT, "deploy/ios_testflight_export.plist")
        self.assertTrue(os.path.exists(export_plist_path))

        with open(export_plist_path, "rb") as f:
            export_data = plistlib.load(f)

        self.assertEqual(export_data.get("method"), "app-store-connect")
        self.assertEqual(export_data.get("destination"), "upload")
        self.assertTrue(export_data.get("stripSwiftSymbols"))

    def test_kubernetes_helm_chart_and_manifests_validity(self) -> None:
        """Validates Helm chart definition and values for 1M CCU cluster."""
        helm_dir = os.path.join(PROJECT_ROOT, "deploy/helm/freeexile")
        chart_path = os.path.join(helm_dir, "Chart.yaml")
        values_path = os.path.join(helm_dir, "values.yaml")

        self.assertTrue(os.path.exists(chart_path), "Chart.yaml missing")
        self.assertTrue(os.path.exists(values_path), "values.yaml missing")

        with open(chart_path, "r", encoding="utf-8") as f:
            chart = yaml.safe_load(f)
        self.assertEqual(chart.get("name"), "freeexile")
        self.assertEqual(chart.get("apiVersion"), "v2")

        with open(values_path, "r", encoding="utf-8") as f:
            values = yaml.safe_load(f)
        self.assertEqual(values["gateway"]["replicaCount"], 50)
        self.assertEqual(values["worldShard"]["replicaCount"], 100)

    def test_kubernetes_manifest_templates_rendered_correctly(self) -> None:
        """Validates all Helm Kubernetes template manifests exist and have valid structure."""
        tmpl_dir = os.path.join(PROJECT_ROOT, "deploy/helm/freeexile/templates")
        expected_templates = [
            "gateway-deployment.yaml",
            "world-statefulset.yaml",
            "hpa.yaml",
            "service.yaml",
        ]
        for tmpl_name in expected_templates:
            tmpl_path = os.path.join(tmpl_dir, tmpl_name)
            self.assertTrue(os.path.exists(tmpl_path), f"Missing template: {tmpl_name}")
            with open(tmpl_path, "r", encoding="utf-8") as f:
                content = f.read()
            self.assertIn("apiVersion:", content)
            self.assertIn("kind:", content)


class TestSprint5RoadmapGovernanceAndTimestamps(unittest.TestCase):
    """Test suite for Sprint Completion Timestamps and Gold Master Release Report."""

    def test_all_sprints_have_completion_timestamps(self) -> None:
        """Verifies all Sprints (1, 2, 3, 4, 5) have valid ISO-8601 completion timestamps."""
        roadmap_path = os.path.join(PROJECT_ROOT, "docs/operations/ROADMAP_2_MONTHS.md")
        self.assertTrue(os.path.exists(roadmap_path))

        with open(roadmap_path, "r", encoding="utf-8") as f:
            content = f.read()

        sprint_headers = re.findall(
            r"### SPRINT (\d):.*?\[HOÀN THÀNH - (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+\d{2}:\d{2})\]",
            content,
        )
        completed_sprints = {int(num): ts for num, ts in sprint_headers}

        for sprint_num in [1, 2, 3, 4, 5]:
            self.assertIn(
                sprint_num,
                completed_sprints,
                f"Sprint {sprint_num} missing completion timestamp [HOÀN THÀNH - YYYY-MM-DDTHH:MM:SS+07:00]",
            )

    def test_timestamps_are_chronologically_valid(self) -> None:
        """Verifies that sprint timestamps follow a chronological progression."""
        roadmap_path = os.path.join(PROJECT_ROOT, "docs/operations/ROADMAP_2_MONTHS.md")
        with open(roadmap_path, "r", encoding="utf-8") as f:
            content = f.read()

        matches = re.findall(
            r"### SPRINT (\d):.*?\[HOÀN THÀNH - ([\d\-T:+]+)\]",
            content,
        )
        self.assertEqual(len(matches), 5)
        timestamps = [m[1] for m in matches]
        for i in range(len(timestamps) - 1):
            self.assertLessEqual(
                timestamps[i],
                timestamps[i + 1],
                f"Sprint {i + 1} timestamp ({timestamps[i]}) > Sprint {i + 2} ({timestamps[i + 1]})",
            )

    def test_gold_master_release_report_exists_and_valid(self) -> None:
        """Verifies the Diátaxis-compliant Gold Master Release Report exists with all required sections."""
        report_path = os.path.join(PROJECT_ROOT, "docs/releases/GOLD_MASTER_RELEASE_REPORT_V1_0_0.md")
        self.assertTrue(os.path.exists(report_path), "Gold Master Release Report missing")

        with open(report_path, "r", encoding="utf-8") as f:
            content = f.read()

        self.assertIn("DOC-REL-001", content)
        self.assertIn("1.000.000 CCU", content)
        self.assertIn("ProMotion 120Hz", content)
        self.assertIn("TestFlight", content)
        self.assertIn("file:///c:/Projects/FreeExile", content)


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