name: Backend Tests

on:
  pull_request:
    paths:
      - "src/api/**"
      - ".github/workflows/backend-tests.yml"
  push:
    branches:
      - main
    paths:
      - "src/api/**"
      - ".github/workflows/backend-tests.yml"
  workflow_dispatch:

permissions:
  contents: read

jobs:
  backend-tests:
    name: Run backend tests
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Check out repository
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.11"
          cache: pip
          cache-dependency-path: src/api/requirements.txt

      - name: Restore testmon cache
        uses: actions/cache@v4
        with:
          path: |
            src/api/.testmondata
            src/api/.pytest_cache
          key: testmon-${{ runner.os }}-py311-${{ github.event_name == 'pull_request' && format('pr-{0}', github.head_ref) || format('main-{0}', github.ref_name) }}-${{ hashFiles('src/api/requirements.txt') }}
          restore-keys: |
            testmon-${{ runner.os }}-py311-main-
            testmon-${{ runner.os }}-py311-

      - name: Install dependencies
        working-directory: src/api
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install coverage pytest-testmon

      - name: Select test targets (PR only)
        if: github.event_name == 'pull_request'
        env:
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        run: |
          python - <<'PY'
          from __future__ import annotations

          import os
          import pathlib
          import subprocess

          base_sha = os.environ.get("BASE_SHA")
          head_sha = os.environ.get("HEAD_SHA")
          if not base_sha or not head_sha:
              raise SystemExit("Missing BASE_SHA or HEAD_SHA")

          changed = subprocess.check_output(
              ["git", "diff", "--name-only", base_sha, head_sha],
              text=True,
          ).splitlines()

          targets: set[str] = set()
          full_run = False
          skip_run = False

          def normalize(path: str) -> str:
              prefix = "src/api/"
              return path[len(prefix):] if path.startswith(prefix) else path

          def add_target(path: str) -> None:
              candidate = normalize(path)
              if pathlib.Path(candidate).exists():
                  targets.add(candidate)

          def is_control_plane_backend_path(path: str) -> bool:
              if path in {
                  "src/api/AGENTS.md",
                  "src/api/CLAUDE.md",
                  "src/api/SKILL.md",
                  ".github/workflows/backend-tests.yml",
              }:
                  return True
              return path.startswith("src/api/scripts/")

          for path in changed:
              if path in {
                  "src/api/requirements.txt",
              }:
                  full_run = True
                  break
              if not path.startswith("src/api/"):
                  if path == ".github/workflows/backend-tests.yml":
                      continue
                  continue
              if is_control_plane_backend_path(path):
                  continue
              if path.startswith("src/api/migrations/"):
                  full_run = True
                  break
              if path.startswith("src/api/tests/"):
                  add_target(path)
                  continue
              if path.startswith("src/api/flaskr/service/"):
                  parts = path.split("/")
                  if len(parts) > 4:
                      module = parts[4]
                      test_dir = pathlib.Path("tests/service") / module
                      if test_dir.exists():
                          targets.add(str(test_dir))
                      else:
                          full_run = True
                          break
                  else:
                      full_run = True
                      break
                  continue
              # Fallback to full run for non-service changes under src/api.
              full_run = True
              break

          if full_run:
              targets = {"tests"}
          elif not targets:
              skip_run = True

          targets_list = " ".join(sorted(targets))
          if skip_run:
              print("Selected targets: <skipped>")
          else:
              print(f"Selected targets: {targets_list}")
          with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env_file:
              env_file.write(f"SKIP_BACKEND_TESTS={'1' if skip_run else '0'}\n")
              env_file.write(f"TEST_TARGETS={targets_list}\n")
          with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
              summary.write("### Backend test targets (PR)\n")
              if skip_run:
                  summary.write("- Targets: `skipped`\n")
                  summary.write("- Reason: control-plane or tooling-only backend changes.\n")
              else:
                  summary.write(f"- Targets: `{targets_list}`\n")
          PY

      - name: Skip backend tests (PR control-plane only)
        if: github.event_name == 'pull_request' && env.SKIP_BACKEND_TESTS == '1'
        run: |
          echo "Skipping backend tests because the PR only changed backend control-plane or tooling files."

      - name: Run tests (PR incremental)
        if: github.event_name == 'pull_request' && env.SKIP_BACKEND_TESTS != '1'
        working-directory: src/api
        run: |
          python -m pytest --testmon $TEST_TARGETS

      - name: Run tests with coverage (main/full)
        if: github.event_name != 'pull_request'
        working-directory: src/api
        run: |
          python -m coverage run -m pytest --testmon-noselect
          python -m coverage report -m
          python -m coverage report -m >> "$GITHUB_STEP_SUMMARY"
