name: Integration Tests

on:
  push:
    branches: [main, master]
    paths:
      - "src/freecad_mcp/**/*.py"
      - "macros/**/*.FCMacro"
      - "macros/**/*.py"
      - "addon/FreecadRobustMCP/**/*.py"
      - "tests/integration/**/*.py"
      - ".github/workflows/macro-test.yaml"
  pull_request:
    branches: [main, master]
    paths:
      - "src/freecad_mcp/**/*.py"
      - "macros/**/*.FCMacro"
      - "macros/**/*.py"
      - "addon/FreecadRobustMCP/**/*.py"
      - "tests/integration/**/*.py"
      - ".github/workflows/macro-test.yaml"
  workflow_dispatch:
  workflow_call:

# Cancel in-progress runs for the same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test-integration:
    name: Integration Tests with FreeCAD
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Install mise
        uses: jdx/mise-action@v3

      - name: Get latest FreeCAD release info
        id: freecad-release
        run: |
          # Get the latest stable release tag from GitHub API
          RELEASE_INFO=$(curl -s https://api.github.com/repos/FreeCAD/FreeCAD/releases/latest)
          TAG_NAME=$(echo "$RELEASE_INFO" | jq -r '.tag_name')
          echo "Latest FreeCAD release: $TAG_NAME"
          echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT"

          # Find the Linux AppImage asset URL
          APPIMAGE_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name | test("Linux-x86_64.*\\.AppImage$")) | .browser_download_url' | head -1)
          APPIMAGE_NAME=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name | test("Linux-x86_64.*\\.AppImage$")) | .name' | head -1)

          if [ -z "$APPIMAGE_URL" ] || [ "$APPIMAGE_URL" = "null" ]; then
              echo "ERROR: Could not find Linux AppImage in release assets"
              exit 1
          fi

          echo "AppImage URL: $APPIMAGE_URL"
          echo "AppImage name: $APPIMAGE_NAME"
          echo "url=$APPIMAGE_URL" >> "$GITHUB_OUTPUT"
          echo "name=$APPIMAGE_NAME" >> "$GITHUB_OUTPUT"

      - name: Cache FreeCAD AppImage
        id: cache-freecad
        uses: actions/cache@v5
        with:
          path: ~/freecad-appimage
          key: freecad-appimage-${{ steps.freecad-release.outputs.tag }}

      - name: Download FreeCAD AppImage
        if: steps.cache-freecad.outputs.cache-hit != 'true'
        run: |
          mkdir -p ~/freecad-appimage
          echo "Downloading FreeCAD ${{ steps.freecad-release.outputs.tag }}..."
          curl -L -o ~/freecad-appimage/FreeCAD.AppImage "${{ steps.freecad-release.outputs.url }}"
          chmod +x ~/freecad-appimage/FreeCAD.AppImage

      - name: Setup FreeCAD AppImage
        run: |
          # Make AppImage executable (in case restored from cache)
          chmod +x ~/freecad-appimage/FreeCAD.AppImage

          # Extract AppImage for headless use (AppImages need FUSE which isn't available in CI)
          cd ~/freecad-appimage
          ./FreeCAD.AppImage --appimage-extract > /dev/null 2>&1

          # Create symlinks for easy access
          sudo ln -sf ~/freecad-appimage/squashfs-root/usr/bin/freecadcmd /usr/local/bin/freecadcmd
          sudo ln -sf ~/freecad-appimage/squashfs-root/usr/bin/freecad /usr/local/bin/freecad

      - name: Verify FreeCAD installation
        run: |
          echo "Checking FreeCAD installation..."
          freecadcmd --version || freecad --version || echo "Version check failed"
          which freecadcmd
          # Show Python version bundled with FreeCAD
          freecadcmd -c "import sys; print(f'FreeCAD Python: {sys.version}')" || true

      - name: Cache uv dependencies
        uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true
          cache-dependency-glob: "**/uv.lock"

      - name: Install dependencies
        run: uv sync --all-extras

      - name: Validate macro syntax
        run: |
          echo "Validating macro Python syntax..."
          for macro in macros/**/*.FCMacro; do
              echo "Checking: $macro"
              python3 -m py_compile "$macro" || {
                  echo "ERROR: Syntax error in $macro"
                  exit 1
              }
          done
          echo "All macros have valid Python syntax"

      - name: Start FreeCAD headless with MCP bridge
        run: |
          echo "Using FreeCAD: freecadcmd"

          # Start FreeCAD headless with MCP bridge in background
          # Uses the workbench addon's headless server script
          freecadcmd addon/FreecadRobustMCP/freecad_mcp_bridge/headless_server.py > /tmp/freecad_bridge.log 2>&1 &
          FREECAD_PID=$!
          echo "FREECAD_PID=$FREECAD_PID" >> "$GITHUB_ENV"

          # Wait for bridge to be ready (check XML-RPC port)
          echo "Waiting for MCP bridge to start..."
          for i in {1..60}; do
              if curl -s --max-time 2 -X POST \
                  -H "Content-Type: text/xml" \
                  -d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
                  http://localhost:9875 > /dev/null 2>&1; then
                  echo "MCP bridge is ready (took ${i}s)"
                  break
              fi
              if [ "$i" -eq 60 ]; then
                  echo "ERROR: MCP bridge did not start within 60s"
                  echo "=== Bridge log ==="
                  cat /tmp/freecad_bridge.log || true
                  kill "$FREECAD_PID" 2>/dev/null || true
                  exit 1
              fi
              sleep 1
          done

          # Log the instance ID for debugging (pytest tests verify bridge via conftest.py fixtures)
          BRIDGE_INSTANCE_ID=$(grep -o 'FREECAD_MCP_BRIDGE_INSTANCE_ID=[^ ]*' /tmp/freecad_bridge.log | cut -d= -f2 | head -1)
          if [ -n "$BRIDGE_INSTANCE_ID" ]; then
              echo "Bridge Instance ID: $BRIDGE_INSTANCE_ID"
          else
              echo "Warning: Could not extract bridge instance ID from log"
              cat /tmp/freecad_bridge.log || true
          fi

      - name: Run CutObjectForMagnets macro tests
        env:
          FREECAD_MODE: xmlrpc
        # Note: Tests use boolean holes (not PartDesign::Hole) for CI compatibility.
        # PartDesign::Hole has a CADKernelError bug in some headless AppImage environments.
        run: |
          uv run pytest tests/integration/test_cut_object_for_magnets.py -v --tb=short

      - name: Run MultiExport macro tests
        env:
          FREECAD_MODE: xmlrpc
        # Export functionality should work in headless mode
        continue-on-error: true
        run: |
          uv run pytest tests/integration/test_multi_export.py -v --tb=short

      - name: Run headless mode integration tests
        env:
          FREECAD_MODE: xmlrpc
        # Tests for headless FreeCAD operations (primitives, booleans, exports, etc.)
        run: |
          uv run pytest tests/integration/test_headless_mode.py -v --tb=short

      - name: Stop FreeCAD
        if: always()
        run: |
          if [ -n "$FREECAD_PID" ]; then
              kill "$FREECAD_PID" 2>/dev/null || true
          fi

  lint-macros:
    name: Lint Macro Files
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Install mise
        uses: jdx/mise-action@v3

      - name: Cache uv dependencies
        uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true
          cache-dependency-glob: "**/uv.lock"

      - name: Install dependencies
        run: uv sync --all-extras

      - name: Lint macro files with Ruff
        run: |
          # Run ruff on macro files (check only, don't fail on issues specific to FreeCAD macros)
          uv run ruff check macros/ --select=E,F,W --ignore=F401,E402 || true

      - name: Check macro documentation
        run: |
          # Ensure each macro directory has a README
          for macro_dir in macros/*/; do
              if [ -d "$macro_dir" ]; then
                  readme_found=false
                  for readme in "$macro_dir"README*.md; do
                      if [ -f "$readme" ]; then
                          readme_found=true
                          break
                      fi
                  done
                  if [ "$readme_found" = false ]; then
                      echo "WARNING: No README found in $macro_dir"
                  else
                      echo "OK: README found in $macro_dir"
                  fi
              fi
          done
