name: E2E Tests

# This workflow executes end-to-end tests against a real Immich server.
# It only runs via workflow_dispatch, and all sensitive jobs are gated by environments.

on:
  workflow_dispatch:
    inputs:
      pr_number:
        description: "Pull request number"
        required: true
      ref:
        description: "PR head ref (branch name)"
        required: true
      head_repo:
        description: "owner/repo of the PR head"
        required: true
      head_sha:
        description: "Head commit SHA approved for testing"
        required: true
      base_ref:
        description: "Base branch that hosts the workflow"
        required: true
      trusted:
        description: "Set to true when the PR author is trusted"
        required: true

# Global environment variables
env:
  project_dir: /home/runner/work/immich-go/immich-go
  e2e_folder: /home/runner/work/immich-go/immich-go/internal/e2e/testdata/immich-server
  e2e_users: /home/runner/work/immich-go/immich-go/internal/e2e/testdata/immich-server/e2eusers.env
  e2e_server: e2e-immich-${{ github.run_id }}
  e2e_url: http://e2e-immich-${{ github.run_id }}:2283
  e2e_ssh: root@e2e-immich-${{ github.run_id }}
  runner_timeout: 900
  pr_number: ${{ inputs.pr_number }}
  pr_ref: ${{ inputs.ref }}
  pr_repo: ${{ inputs.head_repo }}
  pr_sha: ${{ inputs.head_sha }}
  pr_base_ref: ${{ inputs.base_ref }}
  pr_trusted: ${{ inputs.trusted }}

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: e2e-tests-${{ inputs.pr_number }}
  cancel-in-progress: false

jobs:
  prepare:
    name: 🔎 Verify Request
    runs-on: ubuntu-latest
    outputs:
      should_run: ${{ steps.verify.outputs.should_run }}
      ref: ${{ steps.verify.outputs.ref }}
      head_repo: ${{ steps.verify.outputs.head_repo }}
      head_sha: ${{ steps.verify.outputs.head_sha }}
      trusted: ${{ steps.verify.outputs.trusted }}
    steps:
      - name: Verify PR metadata
        id: verify
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          EXPECTED_PR: ${{ inputs.pr_number }}
          EXPECTED_REF: ${{ inputs.ref }}
          EXPECTED_REPO: ${{ inputs.head_repo }}
          EXPECTED_SHA: ${{ inputs.head_sha }}
          TRUSTED: ${{ inputs.trusted }}
          REPO: ${{ github.repository }}
        run: |
          set -euo pipefail

          if [[ -z "$EXPECTED_PR" ]]; then
            echo "❌ pr_number input is required"
            exit 1
          fi

          PR_JSON=$(gh api "/repos/$REPO/pulls/$EXPECTED_PR")
          HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref')
          HEAD_REPO=$(echo "$PR_JSON" | jq -r '.head.repo.full_name')
          HEAD_SHA=$(echo "$PR_JSON" | jq -r '.head.sha')

          if [[ "$HEAD_REPO" != "$EXPECTED_REPO" ]]; then
            echo "❌ Head repository mismatch ($HEAD_REPO != $EXPECTED_REPO). Request a new authorization run."
            exit 1
          fi

          if [[ "$HEAD_REF" != "$EXPECTED_REF" ]]; then
            echo "❌ Head ref changed to $HEAD_REF. Request a new authorization run."
            exit 1
          fi

          if [[ "$HEAD_SHA" != "$EXPECTED_SHA" ]]; then
            echo "❌ Detected new commits on the PR. Please re-run authorization."
            exit 1
          fi

          NON_DOC=$(gh api --paginate "/repos/$REPO/pulls/$EXPECTED_PR/files" --jq '.[].filename' | grep -vE '\\.(md|txt)$|^docs/' || true)
          if [[ -z "$NON_DOC" ]]; then
            echo "⏭️ Documentation-only changes detected after authorization."
            echo "should_run=false" >> $GITHUB_OUTPUT
            echo "trusted=$TRUSTED" >> $GITHUB_OUTPUT
            exit 0
          fi

          echo "should_run=true" >> $GITHUB_OUTPUT
          echo "ref=$HEAD_REF" >> $GITHUB_OUTPUT
          echo "head_repo=$HEAD_REPO" >> $GITHUB_OUTPUT
          echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT
          echo "trusted=$TRUSTED" >> $GITHUB_OUTPUT

  # E2E Server
  e2e-server:
    name: 🖥️ E2E Server
    runs-on: ubuntu-latest
    needs: prepare
    if: needs.prepare.outputs.should_run == 'true'
    environment:
      name: ${{ needs.prepare.outputs.trusted == 'true' && 'e2e-trusted' || 'e2e-infra' }}
    timeout-minutes: 15
    steps:
      - name: Connect to Tailscale
        uses: tailscale/github-action@v4
        with:
          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
          tags: tag:ci-immich-go
          hostname: ${{ env.e2e_server }}
          args: --ssh

      - name: Prepare server environment
        run: |
          mkdir -p "${{ env.e2e_folder }}"
          touch "${{ env.e2e_users }}"

      - name: Deploy Immich
        run: |
          cd "${{ env.e2e_folder }}"
          curl -fsSL "https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml" -o docker-compose.yml
          curl -fsSL "https://github.com/immich-app/immich/releases/latest/download/example.env" -o .env
          docker compose pull -q
          docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans

      - name: Show Immich containers status
        run: docker compose -f "${{ env.e2e_folder }}/docker-compose.yml" ps

      - name: Wait for Immich API
        run: |
          echo "⏳ Waiting for Immich API at ${{ env.e2e_url }}..."
          for i in {1..90}; do
            if curl -sf "${{ env.e2e_url }}/api/server/ping" > /dev/null 2>&1; then
              echo "✅ API is ready (took $((i*2))s)"
              exit 0
            fi
            sleep 2
          done
          echo "❌ Immich API did not become ready within 180 seconds"
          exit 1

      - name: Keep server running for tests
        run: |
          echo "🖥️ Server is running. Waiting for client tests to complete..."
          max_wait=${{ env.runner_timeout }}
          for ((elapsed=0; elapsed<max_wait; elapsed+=5)); do
            if [ -f "${{ env.e2e_folder }}/done" ]; then
              echo "✅ Done marker found! Tests completed."
              exit 0
            fi
            sleep 5
          done
          echo "⚠️ Maximum wait time reached. Shutting down server."
          exit 1

      - name: Show Immich logs on failure
        if: failure()
        run: |
          echo "=== Immich Server Logs ==="
          docker compose -f "${{ env.e2e_folder }}/docker-compose.yml" logs

      - name: Cleanup Immich Instance
        if: always()
        run: |
          docker compose -f "${{ env.e2e_folder }}/docker-compose.yml" down --volumes --remove-orphans || true
          docker system prune -f || true

  # E2E Linux Client
  e2e-linux:
    name: 🐧 E2E Linux
    runs-on: ubuntu-latest
    needs: prepare
    if: needs.prepare.outputs.should_run == 'true'
    environment:
      name: ${{ needs.prepare.outputs.trusted == 'true' && 'e2e-trusted' || 'e2e-infra' }}
    timeout-minutes: 15
    steps:
      # SECURITY: Explicit checkout of PR code (not automatic)
      - name: Checkout PR code
        uses: actions/checkout@v4
        with:
          repository: ${{ needs.prepare.outputs.head_repo }}
          ref: ${{ needs.prepare.outputs.ref }}
          persist-credentials: false # Don't persist git credentials

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache: true

      - name: Connect to Tailscale
        uses: tailscale/github-action@v4
        with:
          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
          tags: tag:ci-immich-go
          hostname: e2e-linux-${{ github.run_id }}

      - name: Wait for Immich API
        run: |
          echo "⏳ Waiting for Immich API at ${{ env.e2e_url }}..."
          for i in {1..90}; do
            if curl -sf "${{ env.e2e_url }}/api/server/ping" > /dev/null 2>&1; then
              echo "✅ API is ready"
              exit 0
            fi
            sleep 2
          done
          echo "❌ API timeout"
          exit 1

      - name: Create admin user and copy to server
        run: |
          mkdir -p "${{ env.e2e_folder }}"
          cd "${{ env.project_dir }}/internal/e2e/e2eUtils/cmd/createUser"
          go run createUser.go create-admin > "${{ env.e2e_folder }}/e2eusers.env"
          scp -o StrictHostKeyChecking=no "${{ env.e2e_folder }}/e2eusers.env" "${{ env.e2e_ssh }}:${{ env.e2e_folder }}/e2eusers.env"

      - name: Run E2E Tests
        run: |
          cd "${{ env.project_dir }}/internal/e2e/client"
          go test -v -tags=e2e -timeout=30m ./...
        env:
          CGO_ENABLED: 0

  # E2E Windows Client (only if Linux succeeds)
  e2e-windows:
    name: 🪟 E2E Windows
    runs-on: windows-latest
    needs: [prepare, e2e-linux]
    if: needs.prepare.outputs.should_run == 'true'
    environment:
      name: ${{ needs.prepare.outputs.trusted == 'true' && 'e2e-trusted' || 'e2e-infra' }}
    timeout-minutes: 15
    env:
      project_dir: ${{ github.workspace }}
      e2e_users_server: /home/runner/work/immich-go/immich-go/internal/e2e/testdata/immich-server/e2eusers.env
    steps:
      # SECURITY: Explicit checkout of PR code
      - name: Checkout PR code
        uses: actions/checkout@v4
        with:
          repository: ${{ needs.prepare.outputs.head_repo }}
          ref: ${{ needs.prepare.outputs.ref }}
          persist-credentials: false

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache: true

      - name: Connect to Tailscale
        uses: tailscale/github-action@v4
        with:
          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
          tags: tag:ci-immich-go
          hostname: e2e-windows-${{ github.run_id }}

      - name: Wait for Immich API
        shell: pwsh
        run: |
          Write-Host "⏳ Waiting for Immich API..."
          foreach ($i in 1..90) {
            try {
              $res = Invoke-WebRequest -Uri "${{ env.e2e_url }}/api/server/ping" -UseBasicParsing -ErrorAction SilentlyContinue
              if ($res.StatusCode -eq 200) {
                Write-Host "✅ API is ready"
                exit 0
              }
            } catch {}
            Start-Sleep -Seconds 2
          }
          Write-Host "❌ API timeout" -ForegroundColor Red
          exit 1

      - name: Copy e2eusers.env from server
        shell: pwsh
        run: |
          New-Item -Path "${{ env.project_dir }}\internal\e2e\testdata\immich-server" -ItemType Directory -Force | Out-Null
          scp -o StrictHostKeyChecking=no "${{ env.e2e_ssh }}:${{ env.e2e_users_server }}" "${{ env.project_dir }}\internal\e2e\testdata\immich-server\e2eusers.env"

      - name: Run Windows Unit Tests
        shell: pwsh
        run: |
          Write-Host "Running Windows unit tests..."
          go test -v -count=1 ./...
        env:
          CGO_ENABLED: 0

      - name: Build for Windows
        shell: pwsh
        run: |
          Write-Host "Building for Windows..."
          go build -o immich-go-windows.exe -ldflags="-s -w" main.go
        env:
          CGO_ENABLED: 0
          GOOS: windows
          GOARCH: amd64

      - name: Verify Windows build
        shell: pwsh
        run: |
          if (Test-Path "immich-go-windows.exe") {
            Write-Host "✅ Windows build successful"
            .\immich-go-windows.exe version
          } else {
            Write-Host "❌ Windows build failed" -ForegroundColor Red
            exit 1
          }

      - name: Run E2E Tests
        shell: pwsh
        run: |
          cd "${{ env.project_dir }}\internal\e2e\client"
          go test -v -tags=e2e -timeout=30m ./...
        env:
          CGO_ENABLED: 0

  # E2E Cleanup
  e2e-cleanup:
    name: 🧹 E2E Cleanup
    runs-on: ubuntu-latest
    needs: [prepare, e2e-linux, e2e-windows]
    if: always() && needs.prepare.outputs.should_run == 'true'
    environment:
      name: ${{ needs.prepare.outputs.trusted == 'true' && 'e2e-trusted' || 'e2e-infra' }}
    timeout-minutes: 2
    steps:
      - name: Connect to Tailscale
        uses: tailscale/github-action@v4
        with:
          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
          tags: tag:ci-immich-go
          hostname: e2e-cleanup-${{ github.run_id }}
          args: --ssh

      - name: Create done marker on server
        run: |
          ssh -o StrictHostKeyChecking=no "${{ env.e2e_ssh }}" "touch ${{ env.e2e_folder }}/done"

  # Cancel workflow if server fails
  e2e-cancel-on-failure:
    name: 🛑 Cancel on Server Failure
    runs-on: ubuntu-latest
    needs: [prepare, e2e-server]
    if: failure() && needs.prepare.outputs.should_run == 'true'
    environment:
      name: ${{ needs.prepare.outputs.trusted == 'true' && 'e2e-trusted' || 'e2e-infra' }}
    timeout-minutes: 2
    steps:
      - name: Cancel workflow
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "❌ E2E server failed. Cancelling remaining jobs..."
          curl -s -X POST \
            -H "Authorization: Bearer $GITHUB_TOKEN" \
            "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"

  # Final status job - always runs to report workflow completion
  e2e-complete:
    name: ✅ E2E Complete
    runs-on: ubuntu-latest
    needs: [prepare, e2e-server, e2e-linux, e2e-windows, e2e-cleanup]
    if: always()
    steps:
      - name: Check if tests were skipped
        id: skip-check
        run: |
          if [[ "${{ needs.prepare.outputs.should_run }}" != "true" ]]; then
            echo "skipped=true" >> $GITHUB_OUTPUT
            echo "📝 E2E tests skipped (doc-only PR or not triggered)"
            exit 0
          fi
          echo "skipped=false" >> $GITHUB_OUTPUT

      - name: Check test results
        if: steps.skip-check.outputs.skipped != 'true'
        env:
          SERVER_STATUS: ${{ needs.e2e-server.result }}
          LINUX_STATUS: ${{ needs.e2e-linux.result }}
          WINDOWS_STATUS: ${{ needs.e2e-windows.result }}
          CLEANUP_STATUS: ${{ needs.e2e-cleanup.result }}
        run: |
          echo "E2E Server: $SERVER_STATUS"
          echo "E2E Linux: $LINUX_STATUS"
          echo "E2E Windows: $WINDOWS_STATUS"
          echo "E2E Cleanup: $CLEANUP_STATUS"

          if [[ "$SERVER_STATUS" != "success" ]] || \
             [[ "$LINUX_STATUS" != "success" ]] || \
             [[ "$WINDOWS_STATUS" != "success" ]]; then
            echo "❌ E2E tests failed"
            exit 1
          fi

          echo "✅ All E2E tests passed!"
