name: E2E Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  # Build frontend once and share across all jobs
  build-frontend:
    name: Build Frontend
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Setup Node.js
        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020  # v4.4.0
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install frontend dependencies
        run: cd frontend && npm ci

      - name: Build frontend
        run: cd frontend && npm run build

      - name: Upload frontend artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
        with:
          name: frontend-dist-e2e
          path: frontend/dist/
          retention-days: 1

  e2e-tests:
    name: End-to-End Tests
    needs: build-frontend
    runs-on: ${{ matrix.os }}

    env:
      GO111MODULE: "on"

    strategy:
      matrix:
        # On PRs: test only ubuntu-latest. On main/push: test all OSes.
        # Go is pinned to "1.25" because go.mod requires go 1.25.5 — older
        # toolchains can't build the module, and auto-downloading 1.25.x under
        # setup-go's module cache collides ("tar: ... File exists").
        os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]') || fromJSON('["ubuntu-latest", "macos-latest", "windows-latest"]') }}
        go-version: ["1.25"]

    steps:
      - name: Checkout code
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Set up Go
        uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff  # v5.6.0
        with:
          go-version: ${{ matrix.go-version }}
          cache: true

      - name: Download dependencies
        run: go mod download

      - name: Verify dependencies
        run: go mod verify

      - name: Download frontend artifact
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          name: frontend-dist-e2e
          path: frontend/dist

      - name: Copy frontend dist for embedding
        shell: bash
        run: |
          mkdir -p web/frontend
          cp -r frontend/dist web/frontend/

      - name: Build mcpproxy binary for tests (Windows)
        if: matrix.os == 'windows-latest'
        env:
          CGO_ENABLED: "0"
        run: go build -tags nogui -o mcpproxy.exe ./cmd/mcpproxy

      - name: Build mcpproxy binary for tests (Unix)
        if: matrix.os != 'windows-latest'
        env:
          CGO_ENABLED: "0"
        run: go build -tags nogui -o mcpproxy ./cmd/mcpproxy

      - name: Run unit tests
        # -short skips the heavy property/timing tests (TestRapidQuarantine* etc.);
        # they run unguarded in the main-only stress-tests job below so coverage
        # is preserved. Bumped to 4m for headroom under -race on slow runners.
        run: go test -v -short -race -timeout 4m -skip "Binary|MCP|E2E|TestInfoEndpoint|TestGracefulShutdownNoPanic|TestSocketInfoEndpoint" ./internal/...

      # Binary tests are too flaky in CI due to server startup timing issues
      # All Binary tests consistently timeout waiting for server to be ready
      # The binary compilation is verified by the build step, and functionality
      # is tested via unit tests and mock-based E2E tests
      # - name: Run Binary tests (Windows)
      #   if: matrix.os == 'windows-latest'
      #   env:
      #     MCPPROXY_BINARY_PATH: ${{ github.workspace }}/mcpproxy.exe
      #   run: go test -v -race -timeout 3m ./internal/server -run 'TestBinary'
      #
      # - name: Run Binary tests (Unix)
      #   if: matrix.os != 'windows-latest'
      #   env:
      #     MCPPROXY_BINARY_PATH: ${{ github.workspace }}/mcpproxy
      #   run: go test -v -race -timeout 3m ./internal/server -run 'TestBinary'

      - name: Run MCP Protocol tests
        run: go test -v -race -timeout 5m ./internal/server -run TestMCP -skip 'TestMCPProtocolWithBinary|TestMCPProtocolComplexWorkflows|TestMCPProtocolToolCalling|TestMCPProtocolEdgeCases'
        env:
          GO_TEST_TIMEOUT: 300s

      - name: Run E2E tests
        run: go test -v -race -timeout 5m ./internal/server -run TestE2E -skip 'TestE2E_ToolDiscovery|TestE2E_ServerDeleteReaddDifferentTools'
        env:
          GO_TEST_TIMEOUT: 300s

      - name: Check for skipped security tests
        shell: bash
        run: |
          echo "Verifying that security-critical authentication tests were not skipped..."

          # Run the security test explicitly and capture output
          go test -v -timeout 2m ./internal/server -run TestE2E_TrayToCore_UnixSocket 2>&1 | tee security_test_output.log

          # Check for skipped API key security tests
          if grep -q "SKIP.*TCP_NoAPIKey_Fail" security_test_output.log; then
            echo "::error::CRITICAL SECURITY: TCP_NoAPIKey_Fail test was skipped!"
            echo "::error::This test verifies TCP connections require API key authentication."
            echo "::error::Skipping this test could allow authentication bypass vulnerabilities."
            echo "::error::See issue #159 for details."
            exit 1
          fi

          if grep -q "SKIP.*TCP_WithAPIKey_Success" security_test_output.log; then
            echo "::error::CRITICAL SECURITY: TCP_WithAPIKey_Success test was skipped!"
            echo "::error::This test verifies TCP connections with valid API keys are accepted."
            echo "::error::Skipping this test could allow authentication bypass vulnerabilities."
            echo "::error::See issue #159 for details."
            exit 1
          fi

          echo "✅ All security tests executed successfully (no skips detected)"

      - name: Run Logging E2E tests
        run: go test -v -race -timeout 5m ./internal/logs -run TestE2E
        env:
          GO_TEST_TIMEOUT: 300s

      - name: Run E2E tests with race detector
        if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.25'
        run: go test -v -race -timeout 10m ./internal/server -run TestE2E
        env:
          GO_TEST_TIMEOUT: 600s

      - name: Run Logging E2E tests with race detector
        if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.25'
        run: go test -v -race -timeout 10m ./internal/logs -run TestE2E
        env:
          GO_TEST_TIMEOUT: 600s

  integration-tests:
    name: Integration Tests
    runs-on: ubuntu-latest
    needs: [build-frontend, e2e-tests]

    env:
      GO111MODULE: "on"

    steps:
      - name: Checkout code
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Set up Go
        uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff  # v5.6.0
        with:
          go-version: "1.25"
          cache: true

      - name: Download frontend artifact
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          name: frontend-dist-e2e
          path: frontend/dist

      - name: Copy frontend dist for embedding
        run: |
          mkdir -p web/frontend
          cp -r frontend/dist web/frontend/

      - name: Build mcpproxy
        env:
          CGO_ENABLED: "0"
        run: go build -tags nogui -o mcpproxy ./cmd/mcpproxy

      - name: Test mcpproxy binary
        run: |
          # Test version command
          ./mcpproxy --version

          # Test help command
          ./mcpproxy --help

          # Test logging functionality
          timeout 10s ./mcpproxy serve --log-to-file --log-level debug --listen :0 || true

          # Verify log file was created in standard OS location
          if [ "$(uname)" = "Linux" ]; then
            if [ -f "$HOME/.local/state/mcpproxy/logs/main.log" ]; then
              echo "✓ Log file created in Linux standard location"
              head -5 "$HOME/.local/state/mcpproxy/logs/main.log"
            else
              echo "⚠ Log file not found in expected location"
            fi
          fi

      - name: Run tests with coverage (Windows)
        if: matrix.os == 'windows-latest'
        shell: pwsh
        run: |
          go test -v -race '-coverprofile=coverage.out' -covermode=atomic ./internal/server -run TestE2E -skip 'TestE2E_ToolDiscovery|TestE2E_ServerDeleteReaddDifferentTools'
          go test -v -race '-coverprofile=coverage-logs.out' -covermode=atomic ./internal/logs -run TestE2E
          go tool cover '-html=coverage.out' -o coverage.html
          go tool cover '-html=coverage-logs.out' -o coverage-logs.html

      - name: Run tests with coverage (Unix)
        if: matrix.os != 'windows-latest'
        run: |
          go test -v -race -coverprofile=coverage.out -covermode=atomic ./internal/server -run TestE2E -skip 'TestE2E_ToolDiscovery|TestE2E_ServerDeleteReaddDifferentTools'
          go test -v -race -coverprofile=coverage-logs.out -covermode=atomic ./internal/logs -run TestE2E
          go tool cover -html=coverage.out -o coverage.html
          go tool cover -html=coverage-logs.out -o coverage-logs.html

      - name: Upload coverage reports
        uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354  # v6.0.1
        with:
          file: ./coverage.out
          flags: e2e-tests
          name: e2e-coverage
          fail_ci_if_error: false

  logging-tests:
    name: Cross-Platform Logging Tests
    needs: build-frontend
    runs-on: ${{ matrix.os }}

    env:
      GO111MODULE: "on"

    strategy:
      matrix:
        # On PRs: test only on ubuntu-latest
        # On main/push: test on all platforms
        os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]') || fromJSON('["ubuntu-latest", "macos-latest", "windows-latest"]') }}
        include:
          - os: ubuntu-latest
            log_path_check: "$HOME/.local/state/mcpproxy/logs"
            log_standard: "XDG Base Directory Specification"
          - os: macos-latest
            log_path_check: "$HOME/Library/Logs/mcpproxy"
            log_standard: "macOS File System Programming Guide"
          - os: windows-latest
            log_path_check: "$env:LOCALAPPDATA\\mcpproxy\\logs"
            log_standard: "Windows Application Data Guidelines"

    steps:
      - name: Checkout code
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Set up Go
        uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff  # v5.6.0
        with:
          go-version: "1.25"
          cache: true

      - name: Download frontend artifact
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          name: frontend-dist-e2e
          path: frontend/dist

      - name: Copy frontend dist for embedding
        shell: bash
        run: |
          mkdir -p web/frontend
          cp -r frontend/dist web/frontend/

      - name: Run logging unit tests
        run: go test -v -race -timeout 2m ./internal/logs

      - name: Run logging E2E tests
        run: go test -v -race -timeout 5m ./internal/logs -run TestE2E

      - name: Build mcpproxy for logging test (Windows)
        if: matrix.os == 'windows-latest'
        env:
          CGO_ENABLED: "0"
        run: go build -tags nogui -o mcpproxy.exe ./cmd/mcpproxy

      - name: Build mcpproxy for logging test (Unix)
        if: matrix.os != 'windows-latest'
        env:
          CGO_ENABLED: "0"
        run: go build -tags nogui -o mcpproxy ./cmd/mcpproxy

      - name: Test OS-specific log directory creation (Unix)
        if: matrix.os != 'windows-latest'
        run: |
          echo "Testing logging on ${{ matrix.os }}"
          echo "Expected log path: ${{ matrix.log_path_check }}"
          echo "OS Standard: ${{ matrix.log_standard }}"

          # Run mcpproxy briefly to create log files
          ./mcpproxy serve --log-to-file --log-level info --listen :0 &
          MCPPROXY_PID=$!
          sleep 5
          kill $MCPPROXY_PID 2>/dev/null || true
          wait $MCPPROXY_PID 2>/dev/null || true

          # Check if log directory was created
          if [ -d "${{ matrix.log_path_check }}" ]; then
            echo "✓ Log directory created successfully"
            ls -la "${{ matrix.log_path_check }}"

            # Check if log file exists and has content
            if [ -f "${{ matrix.log_path_check }}/main.log" ]; then
              echo "✓ Log file created successfully"
              echo "Log file size: $(wc -c < "${{ matrix.log_path_check }}/main.log") bytes"
              echo "First few lines:"
              head -3 "${{ matrix.log_path_check }}/main.log"

              # Verify log contains expected content
              if grep -q "Log directory configured" "${{ matrix.log_path_check }}/main.log"; then
                echo "✓ Log contains expected startup messages"
              else
                echo "⚠ Log missing expected startup messages"
              fi

              if grep -q "${{ matrix.log_standard }}" "${{ matrix.log_path_check }}/main.log"; then
                echo "✓ Log contains OS standard compliance information"
              else
                echo "⚠ Log missing OS standard compliance information"
              fi
            else
              echo "✗ Log file not created"
              exit 1
            fi
          else
            echo "✗ Log directory not created"
            exit 1
          fi

      - name: Test OS-specific log directory creation (Windows)
        if: matrix.os == 'windows-latest'
        shell: pwsh
        run: |
          Write-Host "Testing logging on Windows"
          Write-Host "Expected log path: $env:LOCALAPPDATA\mcpproxy\logs"
          Write-Host "OS Standard: Windows Application Data Guidelines"

          # Run mcpproxy briefly to create log files
          try {
            $process = Start-Process -FilePath "./mcpproxy.exe" -ArgumentList "serve", "--log-to-file", "--log-level", "info", "--listen", ":0" -NoNewWindow -PassThru
            Start-Sleep -Seconds 5
            $process | Stop-Process -Force -ErrorAction SilentlyContinue
          } catch {
            Write-Host "Error starting/stopping mcpproxy: $_"
          }

          # Check if log directory was created
          $logPath = "$env:LOCALAPPDATA\mcpproxy\logs"
          if (-not (Test-Path $logPath)) {
            Write-Host "✗ Log directory not created"
            exit 1
          }

          Write-Host "✓ Log directory created successfully"
          Get-ChildItem $logPath

          # Check if log file exists and has content
          $logFile = Join-Path $logPath "main.log"
          if (-not (Test-Path $logFile)) {
            Write-Host "✗ Log file not created"
            exit 1
          }

          Write-Host "✓ Log file created successfully"
          $fileSize = (Get-Item $logFile).Length
          Write-Host "Log file size: $fileSize bytes"
          Write-Host "First few lines:"
          Get-Content $logFile -Head 3

          # Verify log contains expected content
          $content = Get-Content $logFile -Raw
          if ($content -match "Log directory configured") {
            Write-Host "✓ Log contains expected startup messages"
          } else {
            Write-Host "⚠ Log missing expected startup messages"
          }

          if ($content -match "Windows Application Data Guidelines") {
            Write-Host "✓ Log contains OS standard compliance information"
          } else {
            Write-Host "⚠ Log missing OS standard compliance information"
          }

      - name: Test log rotation
        if: matrix.os == 'ubuntu-latest'
        run: |
          echo "Testing log rotation functionality"
          go test -v -timeout 2m ./internal/logs -run TestE2E_LogRotation

      - name: Test concurrent logging
        if: matrix.os == 'ubuntu-latest'
        run: |
          echo "Testing concurrent logging functionality"
          go test -v -race -timeout 3m ./internal/logs -run TestE2E_ConcurrentLogging

  oauth-tests:
    name: OAuth E2E Tests
    runs-on: ubuntu-latest
    needs: build-frontend

    env:
      GO111MODULE: "on"

    steps:
      - name: Checkout code
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Set up Go
        uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff  # v5.6.0
        with:
          go-version: "1.25"
          cache: true

      - name: Download dependencies
        run: go mod download

      - name: Run OAuth test server unit tests
        run: go test -v -race -timeout 5m ./tests/oauthserver/...

      - name: Run OAuth integration tests
        env:
          OAUTH_INTEGRATION_TESTS: "1"
        run: go test -v -race -timeout 5m ./tests/oauthserver/... -run TestIntegration

  stress-tests:
    name: Stress Tests
    runs-on: ubuntu-latest
    needs: [build-frontend, e2e-tests]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    steps:
      - name: Checkout code
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Set up Go
        uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff  # v5.6.0
        with:
          go-version: "1.25"
          cache: true

      - name: Download frontend artifact
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          name: frontend-dist-e2e
          path: frontend/dist

      - name: Copy frontend dist for embedding
        run: |
          mkdir -p web/frontend
          cp -r frontend/dist web/frontend/

      - name: Run heavy runtime property/timing tests
        run: |
          # These are -short-skipped in the fast unit step; run them here
          # (unguarded, generous timeout) so invariant coverage is preserved.
          go test -v -race -timeout 20m ./internal/runtime \
            -run 'TestRapidQuarantineStateMachine|TestRapidInvariant_ChangedNeverAutoApproved|TestApplyConfig_ListenAddressChange|TestIsDockerAvailable_NegativeTTLShorter|TestIsDockerAvailable_PositiveTTLHonored|TestToolCacheInvalidation_DisableServerRemovesTools|TestRuntimeStatusSnapshotReflectsRunningAndListen'

      - name: Run concurrent stress tests
        run: |
          # Run the concurrent test multiple times to catch race conditions
          for i in {1..5}; do
            echo "Stress test iteration $i"
            go test -v -race -timeout 10m ./internal/server -run TestE2E_ConcurrentOperations
          done

      - name: Run memory stress test
        run: |
          # Run tests with memory limit
          GOMAXPROCS=1 GOMEMLIMIT=100MiB go test -v -timeout 15m ./internal/server -run TestE2E
