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"
      - ".github/actions/setup-freecad/**"
  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"
      - ".github/actions/setup-freecad/**"
  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
    timeout-minutes: 30

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

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

      - name: Setup FreeCAD
        uses: ./.github/actions/setup-freecad

      - 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 blocking bridge script
          # Use setsid to create a new process group for reliable cleanup
          setsid freecadcmd addon/FreecadRobustMCP/freecad_mcp_bridge/blocking_bridge.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: Upload FreeCAD logs on failure
        if: failure() || cancelled()
        uses: actions/upload-artifact@v6
        with:
          name: freecad-headless-logs
          path: /tmp/freecad_bridge.log
          retention-days: 7
          if-no-files-found: ignore

      - name: Stop FreeCAD
        if: always()
        run: |
          if [ -n "$FREECAD_PID" ]; then
              # Kill process group (handles child processes spawned by FreeCAD/AppImage)
              kill -- "-$FREECAD_PID" 2>/dev/null || kill "$FREECAD_PID" 2>/dev/null || true
          fi

  test-gui:
    name: GUI Tests with FreeCAD + Xvfb
    runs-on: ubuntu-latest
    timeout-minutes: 30
    # FreeCAD GUI requires a window manager to generate expose/configure events.
    # Without a WM, the GUI binary hangs during Qt initialization.
    # Solution: Xvfb + openbox window manager + xdotool for synthetic events.

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

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

      # Note: apt cache removed - caching /var/cache/apt/archives causes permission
      # issues and is largely negated by the immediate sudo apt-get update

      - name: Install Xvfb, openbox, and X11 dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            xvfb \
            openbox \
            xdotool \
            libxkbcommon-x11-0 \
            libxcb-icccm4 \
            libxcb-image0 \
            libxcb-keysyms1 \
            libxcb-randr0 \
            libxcb-render-util0 \
            libxcb-xinerama0 \
            libxcb-xfixes0 \
            libxcb-shape0 \
            libxcb-cursor0 \
            x11-utils \
            libegl1 \
            libgl1-mesa-dri \
            libgl1 \
            mesa-utils \
            fontconfig \
            fonts-dejavu-core
          # Rebuild font cache (required after installing fonts, especially from cache)
          sudo fc-cache -f -v

      - name: Setup FreeCAD
        uses: ./.github/actions/setup-freecad

      - name: Start Xvfb and openbox
        run: |
          echo "Starting Xvfb virtual display..."
          # Use -nolisten tcp to prevent TCP listeners for security
          Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
          XVFB_PID=$!
          echo "XVFB_PID=$XVFB_PID" >> "$GITHUB_ENV"
          echo "DISPLAY=:99" >> "$GITHUB_ENV"
          sleep 2
          # Verify Xvfb is running
          if ! ps -p $XVFB_PID > /dev/null; then
              echo "ERROR: Xvfb failed to start"
              exit 1
          fi
          echo "Xvfb started on display :99 (PID: $XVFB_PID)"

          # Start openbox window manager (required for FreeCAD GUI)
          # FreeCAD needs a WM to generate expose/configure events
          echo "Starting openbox window manager..."
          DISPLAY=:99 openbox &
          OPENBOX_PID=$!
          echo "OPENBOX_PID=$OPENBOX_PID" >> "$GITHUB_ENV"
          sleep 1
          if ! ps -p $OPENBOX_PID > /dev/null; then
              echo "WARNING: openbox may have failed to start"
          else
              echo "openbox started (PID: $OPENBOX_PID)"
          fi

      - name: Verify X11 display
        env:
          DISPLAY: ":99"
        run: |
          echo "Checking X11 display..."
          xdpyinfo | head -10 || echo "xdpyinfo not available"

      - 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: Verify fontconfig setup
        run: |
          echo "Checking fontconfig configuration..."
          # Verify the system fontconfig config exists
          if [ -f /etc/fonts/fonts.conf ]; then
              echo "System fontconfig found at /etc/fonts/fonts.conf"
          else
              echo "ERROR: /etc/fonts/fonts.conf not found!"
              ls -la /etc/fonts/ || echo "/etc/fonts/ directory doesn't exist"
          fi
          # Test fontconfig is working
          echo "Testing fc-list..."
          fc-list | head -5 || echo "fc-list failed"
          echo "Fontconfig setup verified."

      - name: Start FreeCAD GUI with MCP bridge
        env:
          DISPLAY: ":99"
          QT_QPA_PLATFORM: xcb
          # Use software rendering for OpenGL (more reliable in CI)
          LIBGL_ALWAYS_SOFTWARE: "1"
          # Explicit fontconfig paths for AppImage compatibility
          # The AppImage's bundled fontconfig may not find system config automatically
          FONTCONFIG_FILE: /etc/fonts/fonts.conf
          FONTCONFIG_PATH: /etc/fonts
          # Set XDG_RUNTIME_DIR to avoid Qt warning
          XDG_RUNTIME_DIR: /tmp/runtime-root
        run: |
          # Create XDG_RUNTIME_DIR
          mkdir -p "$XDG_RUNTIME_DIR"
          chmod 700 "$XDG_RUNTIME_DIR"

          echo "Starting FreeCAD GUI with MCP bridge under Xvfb + openbox..."
          echo "DISPLAY=$DISPLAY"
          echo "QT_QPA_PLATFORM=$QT_QPA_PLATFORM"
          echo "LIBGL_ALWAYS_SOFTWARE=$LIBGL_ALWAYS_SOFTWARE"
          echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"

          # Verify freecadcmd works (headless mode)
          echo "=== Testing freecadcmd (headless) ==="
          freecadcmd --version || echo "freecadcmd failed"

          # Export fontconfig vars so they're available to backgrounded process
          export FONTCONFIG_FILE FONTCONFIG_PATH

          # Start FreeCAD GUI (not headless) with MCP bridge
          # Uses blocking_bridge.py which blocks with run_forever() to keep process alive
          # GUI features are available since we're using 'freecad' not 'freecadcmd'
          # Use setsid to create a new process group for reliable cleanup
          setsid freecad addon/FreecadRobustMCP/freecad_mcp_bridge/blocking_bridge.py > /tmp/freecad_gui.log 2>&1 &
          FREECAD_PID=$!
          echo "FREECAD_PID=$FREECAD_PID" >> "$GITHUB_ENV"

          echo "Waiting for MCP bridge to start..."
          # Send xdotool events to help FreeCAD GUI initialize
          # FreeCAD needs mouse/keyboard events to process its event queue
          for i in {1..90}; do
              # Send synthetic events to help FreeCAD initialize
              xdotool mousemove $((400 + i*3)) $((300 + i*3)) click 1 key Escape 2>/dev/null || true

              # Check if process is still running
              if ! ps -p "$FREECAD_PID" > /dev/null 2>&1; then
                  echo "ERROR: FreeCAD process died (PID $FREECAD_PID)"
                  echo "=== FreeCAD GUI log ==="
                  cat /tmp/freecad_gui.log || echo "Log file is empty or missing"
                  exit 1
              fi
              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 90 ]; then
                  echo "ERROR: MCP bridge did not start within 90s"
                  echo "=== FreeCAD GUI log ==="
                  cat /tmp/freecad_gui.log || echo "Log file is empty or missing"
                  echo "=== Process status ==="
                  pgrep -a freecad || echo "No freecad processes found"
                  kill -- "-$FREECAD_PID" 2>/dev/null || kill "$FREECAD_PID" 2>/dev/null || true
                  exit 1
              fi
              # Show progress every 10 seconds
              if [ $((i % 10)) -eq 0 ]; then
                  echo "Still waiting... (${i}s)"
                  # Show last few lines of log
                  tail -5 /tmp/freecad_gui.log 2>/dev/null || true
              fi
              sleep 1
          done

          # Verify GUI is available
          sleep 2
          BRIDGE_INSTANCE_ID=$(grep -o 'FREECAD_MCP_BRIDGE_INSTANCE_ID=[^ ]*' /tmp/freecad_gui.log | cut -d= -f2 | head -1)
          echo "Bridge Instance ID: $BRIDGE_INSTANCE_ID"

          # Check if GUI is up
          if grep -q "GuiUp.*True\|GUI.*available" /tmp/freecad_gui.log 2>/dev/null; then
              echo "FreeCAD GUI is available"
          else
              echo "Note: Could not confirm GUI status from log (may still work)"
          fi

      - name: Run GUI mode integration tests
        env:
          FREECAD_MODE: xmlrpc
          DISPLAY: ":99"
        # Tests for GUI-only features (screenshots, visibility, colors, camera, etc.)
        # GUI tests under Xvfb can be flaky; don't fail the workflow during stabilization
        continue-on-error: true
        run: |
          uv run pytest tests/integration/test_gui_mode.py -v --tb=short

      - name: Upload FreeCAD logs on failure
        if: failure() || cancelled()
        uses: actions/upload-artifact@v6
        with:
          name: freecad-gui-logs
          path: /tmp/freecad_gui.log
          retention-days: 7
          if-no-files-found: ignore

      - name: Stop FreeCAD, openbox, and Xvfb
        if: always()
        run: |
          if [ -n "$FREECAD_PID" ]; then
              # Kill process group (handles child processes spawned by FreeCAD/AppImage)
              kill -- "-$FREECAD_PID" 2>/dev/null || kill "$FREECAD_PID" 2>/dev/null || true
          fi
          if [ -n "$OPENBOX_PID" ]; then
              kill "$OPENBOX_PID" 2>/dev/null || true
          fi
          if [ -n "$XVFB_PID" ]; then
              kill "$XVFB_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
