name: CI

on:
  push:
  pull_request:

# Cancel duplicate runs: when a PR exists, the push event is redundant.
# Use head_ref (branch name) for PRs, ref for pushes — same branch collapses to one run.
concurrency:
  group: ci-${{ github.head_ref || github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always

jobs:
  pr-title:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@v6
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo test --all-features
      - run: cargo test --no-default-features
      - name: Verify publishable
        run: cargo publish --dry-run

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt
      - uses: Swatinem/rust-cache@v2
      - run: cargo clippy --all-features -- -D warnings
      - run: cargo fmt --check

  check-features:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo check --all-features
      - run: cargo check --no-default-features
      - run: cargo check --no-default-features --features safety_monitor
      - run: cargo check --no-default-features --features camera
      - run: cargo check --no-default-features --features telescope
      - run: cargo check --no-default-features --features dome

  semver-check:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: obi1kenobi/cargo-semver-checks-action@v2

  validate-specs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 20
      - run: npx @scalar/cli document validate tests/fixtures/AlpacaDeviceAPI_v1.yaml
      - run: npx @scalar/cli document validate tests/fixtures/AlpacaManagementAPI_v1.yaml

  # Determine which ConformU device tests to run based on changed files
  conformu-filter:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    outputs:
      devices: ${{ steps.filter.outputs.devices }}
      any_changed: ${{ steps.filter.outputs.any_changed }}
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Determine affected devices
        id: filter
        env:
          EVENT_NAME: ${{ github.event_name }}
          EVENT_BEFORE: ${{ github.event.before }}
          BASE_REF: ${{ github.base_ref }}
        run: |
          if [ "$EVENT_NAME" = "push" ]; then
            BASE="$EVENT_BEFORE"
          else
            # Ensure base ref is available for comparison
            git fetch origin "$BASE_REF" --depth=1 2>/dev/null || true
            BASE="origin/$BASE_REF"
          fi

          # Get changed files (fallback to full run if diff fails or returns empty)
          CHANGED=$(git diff --name-only "$BASE" HEAD 2>/dev/null) || CHANGED="FULL_RUN"
          if [ -z "$CHANGED" ]; then
            CHANGED="FULL_RUN"
          fi
          echo "Changed files: $CHANGED"

          # Shared files that affect all devices
          SHARED_PATTERNS="src/types/|src/device/|src/registry/|src/discovery/|src/management/|src/lib.rs|src/conformu/dispatch.rs|src/conformu/management.rs"

          ALL_DEVICES='["safetymonitor/0","camera/0","camera/1","switch/0","covercalibrator/0","dome/0","filterwheel/0","focuser/0","observingconditions/0","rotator/0","telescope/0"]'

          if echo "$CHANGED" | grep -qE "$SHARED_PATTERNS" || echo "$CHANGED" | grep -q "FULL_RUN"; then
            echo "devices=$ALL_DEVICES" >> "$GITHUB_OUTPUT"
            echo "any_changed=true" >> "$GITHUB_OUTPUT"
            echo "Shared files changed — testing all devices"
            exit 0
          fi

          # Map file paths to device endpoints
          DEVICES="["
          SEP=""
          SEEN=""

          check_add() {
            local pattern="$1" dev="$2"
            if echo "$CHANGED" | grep -q "$pattern"; then
              if ! echo "$SEEN" | grep -q "$dev"; then
                DEVICES="${DEVICES}${SEP}\"${dev}\""
                SEP=","
                SEEN="${SEEN} ${dev}"
              fi
            fi
          }

          check_add "src/safety_monitor/" "safetymonitor/0"
          check_add "src/conformu/mocks/safety_monitor" "safetymonitor/0"
          check_add "src/camera/" "camera/0"
          check_add "src/conformu/mocks/camera" "camera/0"
          check_add "src/switch/" "switch/0"
          check_add "src/conformu/mocks/switch" "switch/0"
          check_add "src/cover_calibrator/" "covercalibrator/0"
          check_add "src/conformu/mocks/cover_calibrator" "covercalibrator/0"
          check_add "src/dome/" "dome/0"
          check_add "src/conformu/mocks/dome" "dome/0"
          check_add "src/filter_wheel/" "filterwheel/0"
          check_add "src/conformu/mocks/filter_wheel" "filterwheel/0"
          check_add "src/focuser/" "focuser/0"
          check_add "src/conformu/mocks/focuser" "focuser/0"
          check_add "src/observing_conditions/" "observingconditions/0"
          check_add "src/conformu/mocks/observing_conditions" "observingconditions/0"
          check_add "src/rotator/" "rotator/0"
          check_add "src/conformu/mocks/rotator" "rotator/0"
          check_add "src/telescope/" "telescope/0"
          check_add "src/conformu/mocks/telescope" "telescope/0"

          # Camera changes also need color camera test
          if echo "$SEEN" | grep -q "camera/0"; then
            DEVICES="${DEVICES},\"camera/1\""
          fi

          DEVICES="${DEVICES}]"

          if [ "$DEVICES" = "[]" ]; then
            echo "any_changed=false" >> "$GITHUB_OUTPUT"
            echo "devices=[]" >> "$GITHUB_OUTPUT"
            echo "No device files changed — skipping ConformU"
          else
            echo "any_changed=true" >> "$GITHUB_OUTPUT"
            echo "devices=$DEVICES" >> "$GITHUB_OUTPUT"
            echo "Devices to test: $DEVICES"
          fi

  conformu:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    needs: [test, lint, conformu-filter]
    if: needs.conformu-filter.outputs.any_changed == 'true'
    strategy:
      fail-fast: false
      matrix:
        device: ${{ fromJson(needs.conformu-filter.outputs.devices) }}
    env:
      CONFORMU_VERSION: "4.2.1"
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo build
        uses: actions/cache@v5
        with:
          path: |
            target
            ~/.cargo/registry
            ~/.cargo/git
          key: conformu-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}

      - name: Download ConformU
        run: |
          curl -sL "https://github.com/ASCOMInitiative/ConformU/releases/download/v${CONFORMU_VERSION}/conformu.linux-x64.tar.xz" \
            -o conformu.tar.xz
          tar xJf conformu.tar.xz
          chmod +x conformu
          echo "$PWD" >> "$GITHUB_PATH"

      - name: Build and start test harness
        run: |
          cargo build --example conformu_harness --features conformu --release
          cargo run --example conformu_harness --features conformu --release &
          sleep 3
          curl -sf http://127.0.0.1:32888/management/apiversions || (echo "Harness failed to start" && exit 1)

      - name: Run ConformU
        env:
          DEVICE: ${{ matrix.device }}
        run: |
          SLUG=$(echo "$DEVICE" | tr '/' '-')
          conformu conformance "http://127.0.0.1:32888/api/v1/$DEVICE" \
            --resultsfile "conformu-${SLUG}.json" \
            --logfilename "conformu-${SLUG}.log" \
            || true

      - name: Check results
        env:
          DEVICE: ${{ matrix.device }}
        run: |
          SLUG=$(echo "$DEVICE" | tr '/' '-')
          python3 - <<PYEOF
          import json, sys, os
          slug = os.environ["DEVICE"].replace("/", "-")
          f = f"conformu-{slug}.json"
          try:
              data = json.load(open(f))
              errors = data.get("ErrorCount", 0)
              issues = data.get("IssueCount", 0)
              print(f"ConformU {slug}: {errors} errors, {issues} issues")
              if errors > 0:
                  for e in data.get("Errors", []):
                      print(f"  ERROR: {e['Key']}: {e['Value']}")
                  print(f"::error::ConformU found {errors} conformance errors for {slug}")
                  sys.exit(1)
              if issues > 0:
                  for i in data.get("Issues", []):
                      print(f"  ISSUE: {i['Key']}: {i['Value']}")
                  print(f"::error::ConformU found {issues} issues for {slug}")
                  sys.exit(1)
          except Exception as e:
              print(f"::error::Failed to parse results: {e}")
              sys.exit(1)
          PYEOF

      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: conformu-${{ strategy.job-index }}
          path: |
            conformu-*.json
            conformu-*.log
