#!/usr/bin/env python3
"""Dart obfuscation is mandatory for Flutter apps — pin the wiring.

Nothing in this suite can run `flutter build`, so these tests read the action
text the way test_version_override.py does and check that

  * the Flutter release build passes --obfuscate together with
    --split-debug-info (the tool refuses one without the other),
  * the symbol directory is written outside the checkout,
  * the build refuses to ship when no symbol files were produced,
  * the action itself retains the symbols as an artifact — the consumer-side
    autoupdate re-vendors this action but never deploy.yml, so the flag and
    its safety net must not be split across that boundary — and
  * the native Gradle path — which has no Dart code — is left alone.
"""

from __future__ import annotations

import re
import unittest
from pathlib import Path

ACTION_DIR = Path(__file__).resolve().parent.parent
ACTION_YML = ACTION_DIR / "action.yml"


def step(name: str) -> str:
    """One composite step, as raw text, from its `- name:` line to the next."""
    text = ACTION_YML.read_text()
    pattern = rf"    - name: {re.escape(name)}\n(?P<body>.*?)(?=\n    - name:|\Z)"
    match = re.search(pattern, text, re.DOTALL)
    if match is None:
        raise AssertionError(f"missing action step: {name}")
    return match.group("body")


def outputs_block() -> str:
    text = ACTION_YML.read_text()
    return text[text.index("\noutputs:\n") : text.index("\nruns:\n")]


class FlutterObfuscationWiringTest(unittest.TestCase):
    def setUp(self) -> None:
        self.flutter = step("Build release Android App Bundle")
        self.gradle = step("Build release Android App Bundle with Gradle")

    def test_the_flutter_release_build_is_obfuscated(self) -> None:
        self.assertIn("--release", self.flutter)
        self.assertIn("--obfuscate", self.flutter)
        # --obfuscate is rejected by the tool without --split-debug-info.
        self.assertIn("--split-debug-info", self.flutter)

    def test_both_flags_are_unconditional(self) -> None:
        # Not inside an `if [ -n ... ]` like --build-name: obfuscation is
        # mandatory, so the flags sit in the base argument list.
        start = self.flutter.index("args=(")
        base_args = self.flutter[start : self.flutter.index("\n        )", start)]
        self.assertIn("--obfuscate", base_args)
        self.assertIn("--split-debug-info", base_args)

    def test_there_is_no_opt_out(self) -> None:
        text = ACTION_YML.read_text()
        self.assertNotIn("--no-obfuscate", text)
        self.assertNotRegex(text, r"inputs\.[a-z-]*obfusc")
        self.assertNotRegex(text, r"vars\.[A-Z_]*OBFUSC")

    def test_symbols_are_written_outside_the_checkout(self) -> None:
        # The iOS action commits bot changes back to the repo; keeping the
        # symbols under RUNNER_TEMP means no step can ever stage them.
        self.assertIn("DART_SYMBOLS_DIR: ${{ runner.temp }}/", self.flutter)
        self.assertIn('--split-debug-info "$DART_SYMBOLS_DIR"', self.flutter)

    def test_the_build_fails_when_no_symbol_files_were_written(self) -> None:
        guard = self.flutter[self.flutter.index('"$DART_SYMBOLS_DIR"/*.symbols') :]
        self.assertIn("::error::", guard[: guard.index("fi\n")])
        self.assertIn("exit 1", guard[: guard.index("fi\n")])
        # ... and the check happens after the build, not before it.
        self.assertLess(
            self.flutter.index('flutter "${args[@]}"'),
            self.flutter.index("*.symbols"),
        )

    def test_the_symbol_directory_reaches_the_workflow(self) -> None:
        self.assertIn("id: flutter_build", self.flutter)
        self.assertIn('echo "symbols_path=$DART_SYMBOLS_DIR" >> "$GITHUB_OUTPUT"', self.flutter)
        outputs = outputs_block()
        self.assertIn("  symbols-path:\n", outputs)
        self.assertIn("${{ steps.flutter_build.outputs.symbols_path }}", outputs)

    def test_the_action_retains_the_symbols_itself(self) -> None:
        retain = step("Retain Dart symbol files")
        self.assertIn("project_kind == 'flutter'", retain)
        self.assertRegex(retain, r"uses: actions/upload-artifact@[0-9a-f]{40}")
        self.assertIn("path: ${{ steps.flutter_build.outputs.symbols_path }}", retain)
        self.assertIn("name: android-symbols-${{ steps.config.outputs.package_name }}", retain)
        self.assertIn("if-no-files-found: error", retain)
        # The repository's retention setting governs; a fixed number cannot be
        # raised to cover the life of a release.
        self.assertNotIn("retention-days", retain)
        # ... after the build that writes them.
        text = ACTION_YML.read_text()
        self.assertLess(
            text.index("- name: Build release Android App Bundle\n"),
            text.index("- name: Retain Dart symbol files"),
        )

    def test_the_gradle_build_is_left_alone(self) -> None:
        # A native app has no Dart code to obfuscate; the flag would be an
        # unknown Gradle argument.
        self.assertNotIn("obfuscate", self.gradle)
        self.assertNotIn("split-debug-info", self.gradle)


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