name: CI

on:
  push:
  pull_request:

jobs:
  install-check:
    runs-on: blacksmith-4vcpu-ubuntu-2404
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: false

      - name: Checkout submodules (retry)
        run: |
          set -euo pipefail
          git submodule sync --recursive
          for attempt in 1 2 3 4 5; do
            if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
              exit 0
            fi
            echo "Submodule update failed (attempt $attempt/5). Retrying…"
            sleep $((attempt * 10))
          done
          exit 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22.x
          check-latest: true

      - name: Runtime versions
        run: |
          node -v
          npm -v

      - name: Capture node path
        run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"

      - name: Enable corepack and pin pnpm
        run: |
          corepack enable
          corepack prepare pnpm@10.23.0 --activate
          pnpm -v

      - name: Install dependencies (frozen)
        env:
          CI: true
        run: |
          export PATH="$NODE_BIN:$PATH"
          which node
          node -v
          pnpm -v
          pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true

  checks:
    runs-on: blacksmith-4vcpu-ubuntu-2404
    strategy:
      fail-fast: false
      matrix:
        include:
          - runtime: node
            task: lint
            command: pnpm lint
          - runtime: node
            task: test
            command: pnpm test
          - runtime: node
            task: build
            command: pnpm build
          - runtime: node
            task: protocol
            command: pnpm protocol:check
          - runtime: node
            task: format
            command: pnpm format
          - runtime: bun
            task: test
            command: bunx vitest run
          - runtime: bun
            task: build
            command: bunx tsc -p tsconfig.json
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: false

      - name: Checkout submodules (retry)
        run: |
          set -euo pipefail
          git submodule sync --recursive
          for attempt in 1 2 3 4 5; do
            if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
              exit 0
            fi
            echo "Submodule update failed (attempt $attempt/5). Retrying…"
            sleep $((attempt * 10))
          done
          exit 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22.x
          check-latest: true

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: Runtime versions
        run: |
          node -v
          npm -v
          bun -v

      - name: Capture node path
        run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"

      - name: Enable corepack and pin pnpm
        run: |
          corepack enable
          corepack prepare pnpm@10.23.0 --activate
          pnpm -v

      - name: Install dependencies
        env:
          CI: true
        run: |
          export PATH="$NODE_BIN:$PATH"
          which node
          node -v
          pnpm -v
          pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true

      - name: Run ${{ matrix.task }} (${{ matrix.runtime }})
        run: ${{ matrix.command }}

  secrets:
    runs-on: blacksmith-4vcpu-ubuntu-2404
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: false

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install detect-secrets
        run: |
          python -m pip install --upgrade pip
          python -m pip install detect-secrets==1.5.0

      - name: Detect secrets
        run: |
          if ! detect-secrets scan --baseline .secrets.baseline; then
            echo "::error::Secret scanning failed. See docs/gateway/security.md#secret-scanning-detect-secrets"
            exit 1
          fi

  checks-windows:
    runs-on: blacksmith-4vcpu-windows-2025
    defaults:
      run:
        shell: bash
    strategy:
      fail-fast: false
      matrix:
        include:
          - runtime: node
            task: lint
            command: pnpm lint
          - runtime: node
            task: test
            command: pnpm test
          - runtime: node
            task: build
            command: pnpm build
          - runtime: node
            task: protocol
            command: pnpm protocol:check
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: false

      - name: Checkout submodules (retry)
        run: |
          set -euo pipefail
          git submodule sync --recursive
          for attempt in 1 2 3 4 5; do
            if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
              exit 0
            fi
            echo "Submodule update failed (attempt $attempt/5). Retrying…"
            sleep $((attempt * 10))
          done
          exit 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22.x
          check-latest: true

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: Runtime versions
        run: |
          node -v
          npm -v
          bun -v

      - name: Capture node path
        run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"

      - name: Enable corepack and pin pnpm
        run: |
          corepack enable
          corepack prepare pnpm@10.23.0 --activate
          pnpm -v

      - name: Install dependencies
        env:
          CI: true
        run: |
          export PATH="$NODE_BIN:$PATH"
          which node
          node -v
          pnpm -v
          pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true

      - name: Run ${{ matrix.task }} (${{ matrix.runtime }})
        run: ${{ matrix.command }}

  checks-macos:
    if: github.event_name == 'pull_request'
    runs-on: macos-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - task: test
            command: pnpm test
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: false

      - name: Checkout submodules (retry)
        run: |
          set -euo pipefail
          git submodule sync --recursive
          for attempt in 1 2 3 4 5; do
            if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
              exit 0
            fi
            echo "Submodule update failed (attempt $attempt/5). Retrying…"
            sleep $((attempt * 10))
          done
          exit 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22.x
          check-latest: true

      - name: Runtime versions
        run: |
          node -v
          npm -v

      - name: Capture node path
        run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"

      - name: Enable corepack and pin pnpm
        run: |
          corepack enable
          corepack prepare pnpm@10.23.0 --activate
          pnpm -v

      - name: Install dependencies
        env:
          CI: true
        run: |
          export PATH="$NODE_BIN:$PATH"
          which node
          node -v
          pnpm -v
          pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true

      - name: Run ${{ matrix.task }}
        run: ${{ matrix.command }}

  macos-app:
    if: github.event_name == 'pull_request'
    runs-on: macos-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - task: lint
            command: |
              swiftlint --config .swiftlint.yml
              swiftformat --lint apps/macos/Sources --config .swiftformat
          - task: build
            command: |
              set -euo pipefail
              for attempt in 1 2 3; do
                if swift build --package-path apps/macos --configuration release; then
                  exit 0
                fi
                echo "swift build failed (attempt $attempt/3). Retrying…"
                sleep $((attempt * 20))
              done
              exit 1
          - task: test
            command: |
              set -euo pipefail
              for attempt in 1 2 3; do
                if swift test --package-path apps/macos --parallel --enable-code-coverage --show-codecov-path; then
                  exit 0
                fi
                echo "swift test failed (attempt $attempt/3). Retrying…"
                sleep $((attempt * 20))
              done
              exit 1
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: false

      - name: Checkout submodules (retry)
        run: |
          set -euo pipefail
          git submodule sync --recursive
          for attempt in 1 2 3 4 5; do
            if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
              exit 0
            fi
            echo "Submodule update failed (attempt $attempt/5). Retrying…"
            sleep $((attempt * 10))
          done
          exit 1

      - name: Select Xcode 26.1
        run: |
          sudo xcode-select -s /Applications/Xcode_26.1.app
          xcodebuild -version

      - name: Install XcodeGen / SwiftLint / SwiftFormat
        run: |
          brew install xcodegen swiftlint swiftformat

      - name: Show toolchain
        run: |
          sw_vers
          xcodebuild -version
          swift --version

      - name: Run ${{ matrix.task }}
        run: ${{ matrix.command }}
  ios:
    if: false # ignore iOS in CI for now
    runs-on: macos-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: false

      - name: Checkout submodules (retry)
        run: |
          set -euo pipefail
          git submodule sync --recursive
          for attempt in 1 2 3 4 5; do
            if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
              exit 0
            fi
            echo "Submodule update failed (attempt $attempt/5). Retrying…"
            sleep $((attempt * 10))
          done
          exit 1

      - name: Select Xcode 26.1
        run: |
          sudo xcode-select -s /Applications/Xcode_26.1.app
          xcodebuild -version

      - name: Install XcodeGen
        run: brew install xcodegen

      - name: Install SwiftLint / SwiftFormat
        run: brew install swiftlint swiftformat

      - name: Show toolchain
        run: |
          sw_vers
          xcodebuild -version
          swift --version

      - name: Generate iOS project
        run: |
          cd apps/ios
          xcodegen generate

      - name: iOS tests
        run: |
          set -euo pipefail
          RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult"
          DEST_ID="$(
            python3 - <<'PY'
          import json
          import subprocess
          import sys
          import uuid

          def sh(args: list[str]) -> str:
            return subprocess.check_output(args, text=True).strip()

          # Prefer an already-created iPhone simulator if it exists.
          devices = json.loads(sh(["xcrun", "simctl", "list", "devices", "-j"]))
          candidates: list[tuple[str, str]] = []
          for runtime, devs in (devices.get("devices") or {}).items():
            for dev in devs or []:
              if not dev.get("isAvailable"):
                continue
              name = str(dev.get("name") or "")
              udid = str(dev.get("udid") or "")
              if not udid or not name.startswith("iPhone"):
                continue
              candidates.append((name, udid))

          candidates.sort(key=lambda it: (0 if "iPhone 16" in it[0] else 1, it[0]))
          if candidates:
            print(candidates[0][1])
            sys.exit(0)

          # Otherwise, create one from the newest available iOS runtime.
          runtimes = json.loads(sh(["xcrun", "simctl", "list", "runtimes", "-j"])).get("runtimes") or []
          ios = [rt for rt in runtimes if rt.get("platform") == "iOS" and rt.get("isAvailable")]
          if not ios:
            print("No available iOS runtimes found.", file=sys.stderr)
            sys.exit(1)

          def version_key(rt: dict) -> tuple[int, ...]:
            parts: list[int] = []
            for p in str(rt.get("version") or "0").split("."):
              try:
                parts.append(int(p))
              except ValueError:
                parts.append(0)
            return tuple(parts)

          ios.sort(key=version_key, reverse=True)
          runtime = ios[0]
          runtime_id = str(runtime.get("identifier") or "")
          if not runtime_id:
            print("Missing iOS runtime identifier.", file=sys.stderr)
            sys.exit(1)

          supported = runtime.get("supportedDeviceTypes") or []
          iphones = [dt for dt in supported if dt.get("productFamily") == "iPhone"]
          if not iphones:
            print("No iPhone device types for iOS runtime.", file=sys.stderr)
            sys.exit(1)

          iphones.sort(
            key=lambda dt: (
              0 if "iPhone 16" in str(dt.get("name") or "") else 1,
              str(dt.get("name") or ""),
            )
          )
          device_type_id = str(iphones[0].get("identifier") or "")
          if not device_type_id:
            print("Missing iPhone device type identifier.", file=sys.stderr)
            sys.exit(1)

          sim_name = f"CI iPhone {uuid.uuid4().hex[:8]}"
          udid = sh(["xcrun", "simctl", "create", sim_name, device_type_id, runtime_id])
          if not udid:
            print("Failed to create iPhone simulator.", file=sys.stderr)
            sys.exit(1)
          print(udid)
          PY
          )"
          echo "Using iOS Simulator id: $DEST_ID"
          xcodebuild test \
            -project apps/ios/Clawdis.xcodeproj \
            -scheme Clawdis \
            -destination "platform=iOS Simulator,id=$DEST_ID" \
            -resultBundlePath "$RESULT_BUNDLE_PATH" \
            -enableCodeCoverage YES

      - name: iOS coverage summary
        run: |
          set -euo pipefail
          RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult"
          xcrun xccov view --report --only-targets "$RESULT_BUNDLE_PATH"

      - name: iOS coverage gate (43%)
        run: |
          set -euo pipefail
          RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult"
          RESULT_BUNDLE_PATH="$RESULT_BUNDLE_PATH" python3 - <<'PY'
          import json
          import os
          import subprocess
          import sys

          target_name = "Clawdis.app"
          minimum = 0.43

          report = json.loads(
            subprocess.check_output(
              ["xcrun", "xccov", "view", "--report", "--json", os.environ["RESULT_BUNDLE_PATH"]],
              text=True,
            )
          )

          target_coverage = None
          for target in report.get("targets", []):
            if target.get("name") == target_name:
              target_coverage = float(target["lineCoverage"])
              break

          if target_coverage is None:
            print(f"Could not find coverage for target: {target_name}")
            sys.exit(1)

          print(f"{target_name} line coverage: {target_coverage * 100:.2f}% (min {minimum * 100:.2f}%)")
          if target_coverage + 1e-12 < minimum:
            sys.exit(1)
          PY

  android:
    runs-on: blacksmith-4vcpu-ubuntu-2404
    strategy:
      fail-fast: false
      matrix:
        include:
          - task: test
            command: ./gradlew --no-daemon :app:testDebugUnitTest
          - task: build
            command: ./gradlew --no-daemon :app:assembleDebug
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: false

      - name: Checkout submodules (retry)
        run: |
          set -euo pipefail
          git submodule sync --recursive
          for attempt in 1 2 3 4 5; do
            if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
              exit 0
            fi
            echo "Submodule update failed (attempt $attempt/5). Retrying…"
            sleep $((attempt * 10))
          done
          exit 1

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - name: Setup Android SDK
        uses: android-actions/setup-android@v3
        with:
          accept-android-sdk-licenses: false

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4

      - name: Install Android SDK packages
        run: |
          yes | sdkmanager --licenses >/dev/null
          sdkmanager --install \
            "platform-tools" \
            "platforms;android-36" \
            "build-tools;36.0.0"

      - name: Run Android ${{ matrix.task }}
        working-directory: apps/android
        run: ${{ matrix.command }}
