"""
Unit tests for FreeExile iOS and Android Native Platform Release Packaging.
Verifies project configurations, manifests, security, entitlements,
and Swift/Kotlin native bridge architectures.
"""

import unittest
import os
import re
import plistlib
import xml.etree.ElementTree as ET
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
PLATFORM_DIR = PROJECT_ROOT / "platform"
IOS_DIR = PLATFORM_DIR / "ios"
ANDROID_DIR = PLATFORM_DIR / "android"


class TestIOSPlatformPackaging(unittest.TestCase):
    """Verifies iOS Xcode project structure, Info.plist, Privacy Manifest, and Swift bridge files."""

    def test_ios_project_structure_exists(self):
        """All required iOS files and directories must exist."""
        required_paths = [
            IOS_DIR / "FreeExile",
            IOS_DIR / "FreeExile" / "Info.plist",
            IOS_DIR / "FreeExile" / "PrivacyInfo.xcprivacy",
            IOS_DIR / "FreeExile" / "AppDelegate.swift",
            IOS_DIR / "FreeExile" / "FreeExileApp.swift",
            IOS_DIR / "FreeExile" / "ViewController.swift",
            IOS_DIR / "FreeExile" / "HapticBridge.swift",
            IOS_DIR / "FreeExile" / "StoreKitManager.swift",
            IOS_DIR / "FreeExile" / "AppAttestManager.swift",
            IOS_DIR / "FreeExile.xcodeproj" / "project.pbxproj",
        ]
        for p in required_paths:
            self.assertTrue(p.exists(), f"Missing iOS project file: {p}")

    def test_ios_info_plist_validity(self):
        """Info.plist must parse as valid XML plist and contain exact required keys."""
        plist_path = IOS_DIR / "FreeExile" / "Info.plist"
        with open(plist_path, "rb") as f:
            plist = plistlib.load(f)

        self.assertEqual(plist.get("CFBundleIdentifier"), "com.freeexile.arpg")
        self.assertEqual(plist.get("CFBundleShortVersionString"), "1.0.0")
        self.assertEqual(plist.get("CFBundleVersion"), "1")
        self.assertTrue(plist.get("UIRequiresFullScreen"), "UIRequiresFullScreen must be true")
        self.assertTrue(plist.get("UIStatusBarHidden"), "UIStatusBarHidden must be true")
        self.assertFalse(
            plist.get("UIViewControllerBasedStatusBarAppearance"),
            "UIViewControllerBasedStatusBarAppearance must be false",
        )
        self.assertFalse(
            plist.get("ITSAppUsesNonExemptEncryption"),
            "ITSAppUsesNonExemptEncryption must be false",
        )

        orientations = plist.get("UISupportedInterfaceOrientations", [])
        self.assertIn("UIInterfaceOrientationLandscapeLeft", orientations)
        self.assertIn("UIInterfaceOrientationLandscapeRight", orientations)

        ats = plist.get("NSAppTransportSecurity", {})
        self.assertTrue(
            ats.get("NSAllowsArbitraryLoadsInWebContent", False)
            or ats.get("NSAllowsLocalNetworking", False)
        )

    def test_ios_privacy_manifest_compliance(self):
        """PrivacyInfo.xcprivacy must comply with Apple May 2024 privacy mandates."""
        privacy_path = IOS_DIR / "FreeExile" / "PrivacyInfo.xcprivacy"
        with open(privacy_path, "rb") as f:
            privacy = plistlib.load(f)

        self.assertFalse(privacy.get("NSPrivacyTracking"), "NSPrivacyTracking must be false")
        self.assertIn("NSPrivacyAccessedAPITypes", privacy)

        accessed_apis = privacy.get("NSPrivacyAccessedAPITypes", [])
        api_type_names = [item.get("NSPrivacyAccessedAPIType") for item in accessed_apis]
        self.assertIn("NSPrivacyAccessedAPITypeUserDefaults", api_type_names)
        self.assertIn("NSPrivacyAccessedAPITypeSystemBootTime", api_type_names)

        for item in accessed_apis:
            reasons = item.get("NSPrivacyAccessedAPITypeReasons", [])
            self.assertGreater(len(reasons), 0, f"Reasons missing for {item.get('NSPrivacyAccessedAPIType')}")

    def test_swift_source_classes_syntax_and_structure(self):
        """Swift files must define designated classes, protocols, and bridge handlers."""
        app_delegate_text = (IOS_DIR / "FreeExile" / "AppDelegate.swift").read_text(encoding="utf-8")
        self.assertIn("class AppDelegate", app_delegate_text)
        self.assertIn(".ambient", app_delegate_text)
        self.assertIn("registerForRemoteNotifications", app_delegate_text)
        self.assertIn("didRegisterForRemoteNotificationsWithDeviceToken", app_delegate_text)

        view_controller_text = (IOS_DIR / "FreeExile" / "ViewController.swift").read_text(encoding="utf-8")
        self.assertIn("class ViewController", view_controller_text)
        self.assertIn("allowsInlineMediaPlayback = true", view_controller_text)
        self.assertIn('"haptic"', view_controller_text)
        self.assertIn('"storeKit"', view_controller_text)
        self.assertIn('"appAttest"', view_controller_text)

        haptic_text = (IOS_DIR / "FreeExile" / "HapticBridge.swift").read_text(encoding="utf-8")
        self.assertIn("class HapticBridge", haptic_text)
        self.assertIn("UIImpactFeedbackGenerator", haptic_text)
        self.assertIn(".heavy", haptic_text)
        self.assertIn(".medium", haptic_text)
        self.assertIn(".rigid", haptic_text)
        self.assertIn(".soft", haptic_text)
        self.assertIn("UINotificationFeedbackGenerator", haptic_text)

        storekit_text = (IOS_DIR / "FreeExile" / "StoreKitManager.swift").read_text(encoding="utf-8")
        self.assertIn("class StoreKitManager", storekit_text)
        self.assertIn("Product.products", storekit_text)
        self.assertIn("product.purchase()", storekit_text)
        self.assertIn("Transaction.currentEntitlements", storekit_text)

        app_attest_text = (IOS_DIR / "FreeExile" / "AppAttestManager.swift").read_text(encoding="utf-8")
        self.assertIn("class AppAttestManager", app_attest_text)
        self.assertIn("DCAppAttestService", app_attest_text)
        self.assertIn("attestKey", app_attest_text)
        self.assertIn("generateAssertion", app_attest_text)

    def test_ios_file_length_caps(self):
        """No iOS file may exceed 500 lines per FreeExile standards."""
        for swift_file in (IOS_DIR / "FreeExile").glob("*.swift"):
            lines = swift_file.read_text(encoding="utf-8").splitlines()
            self.assertLessEqual(len(lines), 500, f"{swift_file.name} exceeds 500 lines: {len(lines)}")


class TestAndroidPlatformPackaging(unittest.TestCase):
    """Verifies Android Gradle project, Manifest, ProGuard rules, and Kotlin bridges."""

    def test_android_project_structure_exists(self):
        """All required Android files must exist."""
        required_paths = [
            ANDROID_DIR / "settings.gradle.kts",
            ANDROID_DIR / "build.gradle.kts",
            ANDROID_DIR / "app" / "build.gradle.kts",
            ANDROID_DIR / "app" / "proguard-rules.pro",
            ANDROID_DIR / "app" / "src" / "main" / "AndroidManifest.xml",
            ANDROID_DIR / "app" / "src" / "main" / "java" / "com" / "freeexile" / "arpg" / "MainActivity.kt",
            ANDROID_DIR / "app" / "src" / "main" / "java" / "com" / "freeexile" / "arpg" / "NativeBridgeInterface.kt",
            ANDROID_DIR / "app" / "src" / "main" / "java" / "com" / "freeexile" / "arpg" / "PlayBillingManager.kt",
            ANDROID_DIR / "app" / "src" / "main" / "java" / "com" / "freeexile" / "arpg" / "PlayIntegrityManager.kt",
            ANDROID_DIR / "app" / "src" / "main" / "java" / "com" / "freeexile" / "arpg" / "HapticFeedbackManager.kt",
        ]
        for p in required_paths:
            self.assertTrue(p.exists(), f"Missing Android file: {p}")

    def test_android_settings_and_build_gradle(self):
        """Gradle scripts must configure compileSdk 35, targetSdk 35, and dependencies."""
        settings_text = (ANDROID_DIR / "settings.gradle.kts").read_text(encoding="utf-8")
        self.assertIn('rootProject.name = "FreeExile"', settings_text)
        self.assertIn('include(":app")', settings_text)

        app_gradle_text = (ANDROID_DIR / "app" / "build.gradle.kts").read_text(encoding="utf-8")
        self.assertIn("compileSdk = 35", app_gradle_text)
        self.assertIn("targetSdk = 35", app_gradle_text)
        self.assertIn("minSdk = 26", app_gradle_text)
        self.assertIn('applicationId = "com.freeexile.arpg"', app_gradle_text)
        self.assertIn("versionCode = 1", app_gradle_text)
        self.assertIn('versionName = "1.0.0"', app_gradle_text)
        self.assertIn("isMinifyEnabled = true", app_gradle_text)
        self.assertIn("androidx.webkit:webkit", app_gradle_text)
        self.assertIn("com.android.billingclient:billing-ktx:7.0.0", app_gradle_text)
        self.assertIn("com.google.android.play:integrity:1.4.0", app_gradle_text)

    def test_android_manifest_validity(self):
        """AndroidManifest.xml must declare required permissions, features, and orientation."""
        manifest_path = ANDROID_DIR / "app" / "src" / "main" / "AndroidManifest.xml"
        tree = ET.parse(manifest_path)
        root = tree.getroot()

        # Check permissions
        permissions = [elem.attrib.get("{http://schemas.android.com/apk/res/android}name") for elem in root.findall("uses-permission")]
        self.assertIn("android.permission.INTERNET", permissions)
        self.assertIn("android.permission.VIBRATE", permissions)
        self.assertIn("com.android.vending.BILLING", permissions)

        # Check hardware features
        features = [elem.attrib.get("{http://schemas.android.com/apk/res/android}name") for elem in root.findall("uses-feature")]
        self.assertIn("android.hardware.touchscreen.multitouch", features)

        gl_features = [elem.attrib.get("{http://schemas.android.com/apk/res/android}glEsVersion") for elem in root.findall("uses-feature") if "{http://schemas.android.com/apk/res/android}glEsVersion" in elem.attrib]
        self.assertIn("0x00030002", gl_features)

        # Check application attributes
        app = root.find("application")
        self.assertIsNotNone(app)
        self.assertEqual(app.attrib.get("{http://schemas.android.com/apk/res/android}hardwareAccelerated"), "true")

        # Check activity
        activity = app.find("activity")
        self.assertIsNotNone(activity)
        self.assertEqual(
            activity.attrib.get("{http://schemas.android.com/apk/res/android}screenOrientation"),
            "sensorLandscape",
        )
        config_changes = activity.attrib.get("{http://schemas.android.com/apk/res/android}configChanges", "")
        self.assertIn("orientation", config_changes)
        self.assertIn("screenSize", config_changes)
        self.assertIn("keyboardHidden", config_changes)

    def test_android_proguard_rules(self):
        """ProGuard rules must preserve JavascriptInterface and billing/integrity packages."""
        proguard_text = (ANDROID_DIR / "app" / "proguard-rules.pro").read_text(encoding="utf-8")
        self.assertIn("JavascriptInterface", proguard_text)
        self.assertIn("com.freeexile.arpg.NativeBridgeInterface", proguard_text)
        self.assertIn("com.android.billingclient.api.**", proguard_text)
        self.assertIn("com.google.android.play.core.integrity.**", proguard_text)

    def test_kotlin_native_bridge_annotations(self):
        """NativeBridgeInterface must expose @JavascriptInterface on haptic, billing, and integrity."""
        bridge_text = (ANDROID_DIR / "app" / "src" / "main" / "java" / "com" / "freeexile" / "arpg" / "NativeBridgeInterface.kt").read_text(encoding="utf-8")
        self.assertIn("@JavascriptInterface", bridge_text)

        # Regex search for methods with @JavascriptInterface
        pattern = r"@JavascriptInterface\s+fun\s+(\w+)"
        annotated_methods = re.findall(pattern, bridge_text)
        self.assertIn("triggerHaptic", annotated_methods)
        self.assertIn("requestPlayIntegrityToken", annotated_methods)
        self.assertIn("launchBillingFlow", annotated_methods)

    def test_kotlin_file_length_caps(self):
        """No Kotlin file may exceed 500 lines per FreeExile standards."""
        kt_dir = ANDROID_DIR / "app" / "src" / "main" / "java" / "com" / "freeexile" / "arpg"
        for kt_file in kt_dir.glob("*.kt"):
            lines = kt_file.read_text(encoding="utf-8").splitlines()
            self.assertLessEqual(len(lines), 500, f"{kt_file.name} exceeds 500 lines: {len(lines)}")


class TestUnifiedMobileBridge(unittest.TestCase):
    """Verifies that unified client JS bridge integrates with both iOS and Android native interfaces."""

    def test_mobile_native_bridge_js_exists(self):
        bridge_js = PROJECT_ROOT / "client" / "webapp" / "js" / "mobile_native_bridge.js"
        self.assertTrue(bridge_js.exists(), "mobile_native_bridge.js must exist")
        content = bridge_js.read_text(encoding="utf-8")
        self.assertIn("webkit.messageHandlers", content)
        self.assertIn("NativeBridge", content)
        self.assertIn("triggerHaptic", content)
        self.assertIn("launchBillingFlow", content)
        self.assertIn("requestAttestationToken", content)


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