# iOS CI (T-iOS-16; hardens the T-iOS-1 skeleton). Three layers, PLAN_IOS_CLIENT §9:
#
#   1. package-tests    — per-package `swift test` + OWN-SOURCES coverage gate
#      (>= 80%, plan §9). NOTE: the plan §9 raw llvm-cov command reads export
#      TOTALS, which count statically-linked DEPENDENCY sources compiled into
#      the test binary (a known flaw, fix assigned to T-iOS-16). The corrected
#      per-package filter lives in ios/IntegrationTests/scripts/coverage-gate.sh
#      (jq keeps only Packages/<P>/Sources/, excluding *Placeholder*).
#   2. app-tests        — xcodegen + xcodebuild test (WebTermTests bundle,
#      iPhone 16 simulator): ViewModels/components of the app glue layer.
#   3. integration-tests — Swift Testing against the REAL Node server. The
#      ServerHarness self-bootstraps `tsx src/server.ts` on an ephemeral
#      loopback port (127.0.0.1:<free-port> is always in the derived Origin
#      whitelist, src/config.ts:187-226 — no ALLOWED_ORIGINS needed). This layer
#      is the standing anti-drift gate between the Swift-replicated protocol
#      and the server implementation — hence the src/** path triggers below.
name: ios

on:
  push:
    paths:
      - "ios/**"
      - ".github/workflows/ios.yml"
      # integration layer guards client<->server protocol drift:
      - "src/**"
      - "package.json"
      - "package-lock.json"
  pull_request:
    paths:
      - "ios/**"
      - ".github/workflows/ios.yml"
      - "src/**"
      - "package.json"
      - "package-lock.json"

jobs:
  # Layer 1: pure-SwiftPM package tests + the 80% own-sources coverage gate.
  # The gate covers exactly the 4 gated packages (plan §9); TestSupport runs
  # tests below without a gate (test doubles are excluded from the gate).
  package-tests:
    runs-on: macos-15
    strategy:
      fail-fast: false
      matrix:
        package: [WireProtocol, SessionCore, HostRegistry, APIClient]
    steps:
      - uses: actions/checkout@v4
      - name: Select Xcode 16.3
        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
      - name: swift test + own-sources coverage gate (>= 80%)
        run: ios/IntegrationTests/scripts/coverage-gate.sh ${{ matrix.package }}

  testsupport-tests:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - name: Select Xcode 16.3
        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
      - name: swift test (TestSupport — no coverage gate, plan §9 gates 4 packages)
        run: swift test --package-path ios/Packages/TestSupport

  # Layer 2: app-target unit tests (WebTermTests, hosted by the app).
  app-tests:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - name: Select Xcode 16.3
        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
      - name: Install XcodeGen
        run: brew install xcodegen
      - name: Generate project
        run: cd ios && xcodegen generate
      - name: xcodebuild test (WebTermTests, iPhone 16 simulator)
        # No CODE_SIGNING_ALLOWED=NO: KeychainHostStoreLiveTests exercises the
        # real data-protection keychain, which returns -34018 for UNSIGNED test
        # hosts; simulator ad-hoc signing needs no certificates. (W5-fix
        # handoff finding, verified locally in the ui-test leg runs.)
        run: |
          xcodebuild -project ios/WebTerm.xcodeproj -scheme WebTerm \
            -destination 'platform=iOS Simulator,name=iPhone 16' \
            test

  # iPad adaptation (T-iPad-1): run the same app suite on an iPad simulator so
  # the adaptive layout (regular size class / NavigationSplitView, T-iPad-2) is
  # exercised in CI, not only the compact iPhone path.
  ipad-tests:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - name: Select Xcode 16.3
        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
      - name: Install XcodeGen
        run: brew install xcodegen
      - name: Generate project
        run: cd ios && xcodegen generate
      - name: xcodebuild test (WebTermTests, iPad Pro 11-inch simulator)
        run: |
          xcodebuild -project ios/WebTerm.xcodeproj -scheme WebTerm \
            -destination 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)' \
            test

  # Layer 3: contract tests against the real Node server (T-iOS-16 test list).
  # npm ci compiles node-pty (needs the Xcode toolchain — present on the
  # runner); the Swift ServerHarness then boots the server itself.
  integration-tests:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - name: Select Xcode 16.3
        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: npm ci (builds node-pty native module)
        run: npm ci
      - name: swift test vs real server (ServerHarness boots tsx src/server.ts)
        run: swift test --package-path ios/IntegrationTests

  # Layer 4 (W5, plan §9): the ONE scripted XCUITest happy path — 配对(手输) →
  # 列表 → attach → 输入 → gate approve — against a live loopback server. The
  # runner process reads WEBTERM_SERVER_URL (delivered via xcodebuild's
  # TEST_RUNNER_ env prefix) and makes its own HTTP assertions against the
  # server (/live-sessions, /live-sessions/:id/preview, held /hook/permission).
  ui-test:
    runs-on: macos-15
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v4
      - name: Select Xcode 16.3
        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: npm ci (builds node-pty native module)
        run: npm ci
      - name: Start web-terminal server on loopback
        run: |
          PORT=3217 BIND_HOST=127.0.0.1 SHELL_PATH=/bin/bash USE_TMUX=0 \
            nohup node_modules/.bin/tsx src/server.ts > uitest-server.log 2>&1 &
          echo $! > uitest-server.pid
          ready=0
          for _ in $(seq 1 30); do
            if curl -sf http://127.0.0.1:3217/live-sessions > /dev/null; then
              ready=1
              break
            fi
            sleep 1
          done
          if [ "$ready" -ne 1 ]; then
            echo "server did not come up on 127.0.0.1:3217" >&2
            cat uitest-server.log >&2
            exit 1
          fi
      - name: Install XcodeGen
        run: brew install xcodegen
      - name: Generate project
        run: cd ios && xcodegen generate
      # The 输入 step taps the KeyBar, which is the terminal's
      # inputAccessoryView — it only exists while the SOFT keyboard is up.
      - name: Disable simulator hardware keyboard (KeyBar rides the soft keyboard)
        run: defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool false
      - name: xcodebuild test (WebTermUITests, iPhone 16 simulator)
        # TEST_RUNNER_<VAR> must be an ENV VAR of the xcodebuild process (it
        # strips the prefix and injects <VAR> into the test-runner process);
        # passing it as a KEY=VALUE argument makes it a build setting, which
        # never reaches the runner (verified empirically, 2026-07-05).
        # NO CODE_SIGNING_ALLOWED=NO here: the app must be (ad-hoc) signed or
        # the data-protection keychain rejects every SecItem call (-34018) and
        # pairing dies at "保存到本机失败" (verified empirically, run 6).
        # Simulator builds sign locally without any certificate.
        env:
          TEST_RUNNER_WEBTERM_SERVER_URL: http://127.0.0.1:3217
        run: |
          xcodebuild -project ios/WebTerm.xcodeproj -scheme WebTermUITests \
            -destination 'platform=iOS Simulator,name=iPhone 16' test
      - name: Server log + shutdown
        if: always()
        run: |
          cat uitest-server.log || true
          kill "$(cat uitest-server.pid)" 2>/dev/null || true

  # Device-matrix floor (plan §9: "iOS 17 最低目标模拟器各一轮"): run the
  # WebTermTests unit bundle on an iOS 17.x simulator runtime.
  # CI-ONLY LEG: GitHub macOS runner images ship older Xcode versions whose
  # iOS 17.x simulator runtime is registered system-wide with CoreSimulator, so
  # Xcode 16.x can build against it. Local machines are NOT expected to
  # download the ~8 GB runtime. If the runner image drops the 17.x runtime,
  # the leg skips WITH A LOUD NOTICE; if the runtime exists, test failures
  # fail the job (no silent pass).
  ios17-floor-tests:
    runs-on: macos-15
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v4
      - name: Select Xcode 16.3 (build toolchain)
        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
      - name: Install XcodeGen
        run: brew install xcodegen
      - name: Generate project
        run: cd ios && xcodegen generate
      - name: Create iOS 17.x simulator (skip-with-notice if runtime absent)
        id: sim17
        run: |
          runtime="$(xcrun simctl list runtimes | grep -Eo 'com\.apple\.CoreSimulator\.SimRuntime\.iOS-17-[0-9]+' | tail -n 1 || true)"
          if [ -z "$runtime" ]; then
            echo "::notice title=iOS 17 floor leg skipped::no iOS 17.x simulator runtime on this runner image (image drift) — the deployment-floor round did NOT run"
            echo "runtime=" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          udid="$(xcrun simctl create 'iPhone 15 iOS17' 'iPhone 15' "$runtime")"
          echo "created $udid with $runtime"
          echo "runtime=$runtime" >> "$GITHUB_OUTPUT"
          echo "udid=$udid" >> "$GITHUB_OUTPUT"
      # No CODE_SIGNING_ALLOWED=NO: KeychainHostStoreLiveTests needs a signed
      # (ad-hoc, certificate-free on simulator) app host — unsigned = -34018.
      - name: xcodebuild test (WebTermTests on the iOS 17.x floor)
        if: steps.sim17.outputs.runtime != ''
        run: |
          xcodebuild -project ios/WebTerm.xcodeproj -scheme WebTerm \
            -destination "platform=iOS Simulator,id=${{ steps.sim17.outputs.udid }}" test
