"""Exercise the offline delivery preflight against synthesized bundles.

The fixtures reproduce the exact refusals Apple sent for a real release — two
ITMS-90023 icon findings and an ITMS-90362 on a broadcast extension whose
RPBroadcastProcessMode sat under NSExtensionAttributes — and the shapes of the
bundles Apple ACCEPTED, so a regression shows up as a changed verdict.
"""
from pathlib import Path
import plistlib
import struct
import sys
import tempfile
import unittest
from unittest import mock
import zlib

sys.path.insert(0, str(Path(__file__).resolve().parent))

import delivery_preflight as preflight  # noqa: E402


def png(width: int, height: int, *, cgbi: bool = False, alpha: bool = True) -> bytes:
    """A real, decodable PNG, optionally carrying Xcode's CgBI chunk first."""
    def chunk(kind: bytes, payload: bytes) -> bytes:
        return (struct.pack(">I", len(payload)) + kind + payload
                + struct.pack(">I", zlib.crc32(kind + payload) & 0xFFFFFFFF))

    colour = 6 if alpha else 2
    header = struct.pack(">IIBBBBB", width, height, 8, colour, 0, 0, 0)
    stride = width * (4 if alpha else 3)
    raw = b"".join(b"\x00" + b"\x7f" * stride for _ in range(height))
    parts = [b"\x89PNG\r\n\x1a\n"]
    if cgbi:
        parts.append(chunk(b"CgBI", b"\x50\x00\x20\x06"))
    parts.append(chunk(b"IHDR", header))
    parts.append(chunk(b"IDAT", zlib.compress(raw)))
    parts.append(chunk(b"IEND", b""))
    return b"".join(parts)


class PngReadingTests(unittest.TestCase):
    def test_reads_a_plain_png(self):
        self.assertEqual(preflight.png_size(png(152, 152)), (152, 152))

    def test_reads_past_the_xcode_cgbi_chunk(self):
        # A fixed IHDR offset returns garbage for exactly the builds that ship.
        self.assertEqual(preflight.png_size(png(167, 167, cgbi=True)), (167, 167))

    def test_reports_alpha(self):
        self.assertTrue(preflight.png_has_alpha(png(1024, 1024, alpha=True)))
        self.assertFalse(preflight.png_has_alpha(png(1024, 1024, alpha=False)))

    def test_rejects_a_non_png(self):
        self.assertIsNone(preflight.png_size(b"not a png"))


class BundleTests(unittest.TestCase):
    def setUp(self):
        temporary = tempfile.TemporaryDirectory(prefix="delivery-preflight-test-")
        self.addCleanup(temporary.cleanup)
        self.root = Path(temporary.name).resolve()
        self.app = self.root / "Runner.app"
        self.app.mkdir()
        self.info = {
            "CFBundleIdentifier": "com.example.app",
            "CFBundleShortVersionString": "2.0.0",
            "CFBundleVersion": "190",
            "CFBundleSupportedPlatforms": ["iPhoneOS"],
            "UIDeviceFamily": [1, 2],
        }

    def write_info(self):
        (self.app / "Info.plist").write_bytes(plistlib.dumps(self.info))

    def flatten(self, name: str, size: int):
        (self.app / name).write_bytes(png(size, size, cgbi=True))

    def catalog(self, *renditions):
        """Stand in for `assetutil --info`, whose real output needs a compiled
        Assets.car. Each entry is (idiom, pixels)."""
        rows = [{"Name": "AppIcon", "Idiom": idiom, "PixelWidth": pixels,
                 "PixelHeight": pixels} for idiom, pixels in renditions]
        patch = mock.patch.object(preflight, "catalog_renditions", return_value=rows)
        patch.start()
        self.addCleanup(patch.stop)

    def extension(self, name: str, extension: dict, *, identifier: str = "",
                  executable: str = "Ext") -> Path:
        appex = self.app / "PlugIns" / name
        appex.mkdir(parents=True)
        (appex / executable).write_bytes(b"\xcf\xfa\xed\xfe")
        (appex / "Info.plist").write_bytes(plistlib.dumps({
            "CFBundleIdentifier": identifier or f"com.example.app.{appex.stem}",
            "CFBundleExecutable": executable,
            "CFBundleShortVersionString": "2.0.0",
            "CFBundleVersion": "190",
            "NSExtension": extension,
        }))
        return appex

    def codes(self, answer: dict) -> list[str]:
        return [item["itms"] for item in answer["failures"]]

    # ── icons ───────────────────────────────────────────────────────────────

    def test_ipad_icons_missing_is_the_two_itms_90023_apple_sent(self):
        self.write_info()
        self.catalog(("phone", 120), ("phone", 180))
        answer = preflight.inspect(self.app, only={"app_icons"})
        self.assertFalse(answer["ok"])
        self.assertEqual(self.codes(answer), ["ITMS-90023", "ITMS-90023"])
        self.assertIn("152x152", answer["failures"][0]["message"])
        self.assertIn("167x167", answer["failures"][1]["message"])

    def test_a_covered_bundle_passes(self):
        self.write_info()
        self.catalog(("phone", 120), ("phone", 180), ("pad", 152), ("pad", 167))
        answer = preflight.inspect(self.app, only={"app_icons"})
        self.assertTrue(answer["ok"], answer["failures"])

    def test_an_iphone_only_bundle_is_not_asked_for_ipad_icons(self):
        self.info["UIDeviceFamily"] = [1]
        self.write_info()
        self.catalog(("phone", 120), ("phone", 180))
        answer = preflight.inspect(self.app, only={"app_icons"})
        self.assertTrue(answer["ok"], answer["failures"])

    def test_a_single_universal_icon_covers_every_family(self):
        # Xcode 14+ generates every idiom from one universal 1024 entry, so
        # actool emits no `pad` rendition and demanding one refuses a correct app.
        self.write_info()
        self.catalog(("universal", 1024))
        answer = preflight.inspect(self.app, only={"app_icons", "marketing_icon"})
        self.assertTrue(answer["ok"], answer["failures"])

    def test_an_unreadable_catalog_judges_only_the_flattened_squares(self):
        # A shipped, Apple-ACCEPTED iPad build carries flat 120 and 152 files and
        # keeps 167 and 180 in the catalog alone.
        self.write_info()
        self.flatten("AppIcon60x60@2x.png", 120)
        self.flatten("AppIcon76x76@2x~ipad.png", 152)
        answer = preflight.inspect(self.app, only={"app_icons"})
        self.assertTrue(answer["ok"], answer["failures"])
        self.assertEqual(answer["asset_catalog"], "unreadable")
        advisory = answer["advisories"][0]["message"]
        self.assertIn("167x167", advisory)
        self.assertIn("180x180", advisory)

    def test_an_unreadable_catalog_still_catches_a_missing_flat_ipad_icon(self):
        self.write_info()
        self.flatten("AppIcon60x60@2x.png", 120)
        answer = preflight.inspect(self.app, only={"app_icons"})
        self.assertEqual(self.codes(answer), ["ITMS-90023"])
        self.assertIn("152x152", answer["failures"][0]["message"])

    def test_a_marketing_icon_is_required_when_the_catalog_has_icons(self):
        self.write_info()
        self.catalog(("phone", 120), ("phone", 180), ("pad", 152), ("pad", 167))
        answer = preflight.inspect(self.app, only={"marketing_icon"})
        self.assertEqual(self.codes(answer), ["ITMS-90704"])

    def test_a_marketing_rendition_satisfies_it(self):
        self.write_info()
        self.catalog(("phone", 120), ("marketing", 1024))
        answer = preflight.inspect(self.app, only={"marketing_icon"})
        self.assertTrue(answer["ok"], answer["failures"])

    # ── extensions ──────────────────────────────────────────────────────────

    def test_broadcast_mode_under_attributes_only_is_refused_and_explained(self):
        self.write_info()
        self.extension("ScreenBroadcast.appex", {
            "NSExtensionPointIdentifier": "com.apple.broadcast-services-upload",
            "NSExtensionPrincipalClass": "SampleHandler",
            "NSExtensionAttributes": {"RPBroadcastProcessMode": "RPBroadcastProcessModeSampleBuffer"},
        })
        answer = preflight.inspect(self.app, only={"extension_keys"})
        self.assertEqual(self.codes(answer), ["ITMS-90362"])
        message = answer["failures"][0]["message"]
        self.assertIn("NSExtension.RPBroadcastProcessMode is absent", message)
        self.assertIn("NSExtensionAttributes", message)

    def test_broadcast_mode_directly_under_nsextension_passes(self):
        self.write_info()
        self.extension("ScreenBroadcast.appex", {
            "NSExtensionPointIdentifier": "com.apple.broadcast-services-upload",
            "NSExtensionPrincipalClass": "SampleHandler",
            "RPBroadcastProcessMode": "RPBroadcastProcessModeSampleBuffer",
        })
        answer = preflight.inspect(self.app, only={"extension_keys"})
        self.assertTrue(answer["ok"], answer["failures"])

    def test_an_illegal_broadcast_mode_value_is_refused(self):
        self.write_info()
        self.extension("ScreenBroadcast.appex", {
            "NSExtensionPointIdentifier": "com.apple.broadcast-services-upload",
            "RPBroadcastProcessMode": "RPBroadcastProcessModeSomethingElse",
        })
        answer = preflight.inspect(self.app, only={"extension_keys"})
        self.assertEqual(self.codes(answer), ["ITMS-90362"])
        self.assertIn("not an accepted value", answer["failures"][0]["message"])

    def test_a_widgetkit_extension_with_only_its_point_identifier_passes(self):
        self.write_info()
        self.extension("SunnahWidget.appex", {
            "NSExtensionPointIdentifier": "com.apple.widgetkit-extension",
        })
        answer = preflight.inspect(self.app, only={"extension_keys"})
        self.assertTrue(answer["ok"], answer["failures"])

    def test_a_widgetkit_principal_class_is_refused_and_explained(self):
        self.write_info()
        self.extension("SunnahWidget.appex", {
            "NSExtensionPointIdentifier": "com.apple.widgetkit-extension",
            "NSExtensionPrincipalClass": "$(PRODUCT_MODULE_NAME).SunnahWidgetBundle",
        })
        answer = preflight.inspect(self.app, only={"extension_keys"})
        self.assertEqual(self.codes(answer), ["Unexpected Info.plist key"])
        failure = answer["failures"][0]
        self.assertIn("NSExtension.NSExtensionPrincipalClass is present", failure["message"])
        self.assertIn("Remove NSExtension.NSExtensionPrincipalClass", failure["next"])
        self.assertIn("@main WidgetBundle", failure["next"])

    def test_an_unknown_extension_point_is_left_alone(self):
        self.write_info()
        self.extension("Other.appex", {"NSExtensionPointIdentifier": "com.example.unknown"})
        answer = preflight.inspect(self.app, only={"extension_keys"})
        self.assertTrue(answer["ok"], answer["failures"])

    # ── identity, versions, hygiene ─────────────────────────────────────────

    def test_an_extension_outside_the_app_id_namespace_is_refused(self):
        self.write_info()
        self.extension("Stray.appex", {"NSExtensionPointIdentifier": "com.example.unknown"},
                       identifier="com.other.stray")
        answer = preflight.inspect(self.app, only={"bundle_identifiers"})
        self.assertEqual(self.codes(answer), ["ITMS-90046"])

    def test_an_unresolved_bundle_id_variable_is_refused(self):
        self.write_info()
        self.extension("Stray.appex", {"NSExtensionPointIdentifier": "com.example.unknown"},
                       identifier="$(PRODUCT_BUNDLE_IDENTIFIER)")
        answer = preflight.inspect(self.app, only={"bundle_identifiers"})
        self.assertEqual(self.codes(answer), ["ITMS-90007"])

    def test_a_vendored_framework_is_never_judged_on_its_identifier(self):
        # CocoaPods duplication in Frameworks/ is normal; only PlugIns is scoped.
        self.write_info()
        frameworks = self.app / "Frameworks" / "Vendor.framework"
        frameworks.mkdir(parents=True)
        (frameworks / "Info.plist").write_bytes(plistlib.dumps(
            {"CFBundleIdentifier": "org.cocoapods.Vendor"}))
        answer = preflight.inspect(self.app, only={"bundle_identifiers"})
        self.assertTrue(answer["ok"], answer["failures"])

    def test_a_version_mismatch_between_app_and_extension_is_refused(self):
        self.write_info()
        appex = self.extension("Late.appex", {"NSExtensionPointIdentifier": "com.example.unknown"})
        values = plistlib.loads((appex / "Info.plist").read_bytes())
        values["CFBundleVersion"] = "189"
        (appex / "Info.plist").write_bytes(plistlib.dumps(values))
        answer = preflight.inspect(self.app, only={"version_strings"})
        self.assertEqual(self.codes(answer), ["ITMS-90473"])

    def test_a_simulator_build_is_refused(self):
        self.info["CFBundleSupportedPlatforms"] = ["iPhoneSimulator"]
        self.write_info()
        answer = preflight.inspect(self.app, only={"payload_hygiene"})
        self.assertIn("ITMS-90085", self.codes(answer))

    def test_a_simulator_extension_inside_a_device_app_is_refused(self):
        self.write_info()
        appex = self.extension("Sim.appex", {"NSExtensionPointIdentifier": "com.example.unknown"})
        values = plistlib.loads((appex / "Info.plist").read_bytes())
        values["CFBundleSupportedPlatforms"] = ["iPhoneSimulator"]
        (appex / "Info.plist").write_bytes(plistlib.dumps(values))
        answer = preflight.inspect(self.app, only={"payload_hygiene"})
        self.assertIn("ITMS-90085", self.codes(answer))

    def test_a_finder_metadata_file_is_refused(self):
        self.write_info()
        (self.app / ".DS_Store").write_bytes(b"\x00\x00\x00\x01")
        answer = preflight.inspect(self.app, only={"payload_hygiene"})
        self.assertIn("ITMS-90049", self.codes(answer))

    def test_a_vendored_framework_modules_directory_is_not_swept_up(self):
        self.write_info()
        modules = self.app / "Frameworks" / "Vendor.framework" / "Modules"
        modules.mkdir(parents=True)
        (modules / "module.modulemap").write_text("framework module Vendor {}")
        answer = preflight.inspect(self.app, only={"payload_hygiene"})
        self.assertTrue(answer["ok"], answer["failures"])

    def test_an_extension_without_its_binary_is_refused(self):
        self.write_info()
        appex = self.extension("Empty.appex", {"NSExtensionPointIdentifier": "com.example.unknown"})
        (appex / "Ext").unlink()
        answer = preflight.inspect(self.app, only={"payload_hygiene"})
        self.assertIn("ITMS-90562", self.codes(answer))

    # ── entry points ────────────────────────────────────────────────────────

    def test_a_missing_bundle_is_a_named_refusal_not_a_traceback(self):
        self.assertEqual(preflight.main([str(self.root / "absent")]), 1)

    def test_the_cli_exits_nonzero_on_a_refusal(self):
        self.write_info()
        self.flatten("AppIcon60x60@2x.png", 120)
        self.assertEqual(preflight.main([str(self.app)]), 1)


class IpaTests(unittest.TestCase):
    """An .ipa is judged without paying for its size."""

    def setUp(self):
        temporary = tempfile.TemporaryDirectory(prefix="delivery-preflight-ipa-")
        self.addCleanup(temporary.cleanup)
        self.root = Path(temporary.name).resolve()

    def build(self, filler: int = 0) -> Path:
        import zipfile as zf
        path = self.root / "App.ipa"
        with zf.ZipFile(path, "w") as archive:
            archive.writestr("Payload/Runner.app/Info.plist", plistlib.dumps({
                "CFBundleIdentifier": "com.example.app",
                "CFBundleShortVersionString": "2.0.0", "CFBundleVersion": "1",
                "CFBundleSupportedPlatforms": ["iPhoneOS"], "UIDeviceFamily": [1]}))
            archive.writestr("Payload/Runner.app/AppIcon60x60@2x.png", png(120, 120, cgbi=True))
            if filler:
                archive.writestr("Payload/Runner.app/Frameworks/Big.framework/Big", b"\0" * filler)
        return path

    def test_the_payload_is_read_without_extracting_the_binary(self):
        ipa = self.build(filler=4 * 1024 * 1024)
        scratch = self.root / "scratch"
        app, names = preflight.find_app(ipa, scratch)
        self.assertIn("Frameworks/Big.framework/Big", names)
        self.assertFalse((app / "Frameworks/Big.framework/Big").exists(),
                         "the binary must not be extracted to judge the bundle")
        self.assertTrue((app / "Info.plist").is_file())

    def test_an_ipa_verdict_matches_the_same_bundle_on_disk(self):
        ipa = self.build()
        app, names = preflight.find_app(ipa, self.root / "scratch")
        answer = preflight.inspect(app, names=names, only={"app_icons"})
        self.assertTrue(answer["ok"], answer["failures"])

    def test_an_ipa_without_a_scratch_directory_is_a_named_refusal(self):
        with self.assertRaises(preflight.Refused):
            preflight.find_app(self.build(), None)


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