# CI/CD工作流配置：构建和测试.NET项目
# 该工作流仅在创建或更新面向任意分支的 pull request 时触发
name: CI - Build & Test

on:
  pull_request:
    branches: [ '**' ]

permissions:
  contents: read
  pull-requests: write
  security-events: write

jobs:
  # 代码质量检查 job（并行执行，不阻塞构建）
  code-quality:
    name: Code Quality & Security
    runs-on: ubuntu-latest

    steps:
      # 检出源代码，设置fetch-depth为0以获取完整的git历史
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      # 校验C#命名空间与源码目录是否符合命名规范
      - name: Validate C# naming
        run: bash scripts/validate-csharp-naming.sh

      # 缓存MegaLinter
      - name: Cache MegaLinter
        uses: actions/cache@v5
        with:
          path: ~/.cache/megalinter
          key: ${{ runner.os }}-megalinter-v9
          restore-keys: |
            ${{ runner.os }}-megalinter-

      # MegaLinter扫描步骤
      # 执行代码质量检查和安全扫描，生成SARIF格式报告
      - name: MegaLinter
        uses: oxsecurity/megalinter@v9.4.0
        continue-on-error: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          FAIL_ON_ERROR: ${{ github.ref == 'refs/heads/main' }}

      # 上传SARIF格式的安全和代码质量问题报告到GitHub安全中心
      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v4
        with:
          sarif_file: megalinter-reports/sarif

      # 缓存TruffleHog
      - name: Cache TruffleHog
        uses: actions/cache@v5
        with:
          path: ~/.cache/trufflehog
          key: ${{ runner.os }}-trufflehog

      # TruffleHog OSS 扫描步骤
      # 使用 TruffleHog 工具扫描代码库中的敏感信息泄露，如API密钥、密码等
      # 该步骤会比较基础分支和当前提交之间的差异，检测新增内容中是否包含敏感数据
      - name: TruffleHog OSS
        uses: trufflesecurity/trufflehog@v3.94.3
        with:
          # 扫描路径，. 表示扫描整个仓库
          path: .
          # 基础提交哈希，用于与当前提交进行比较
          base: ${{ github.event.pull_request.base.sha }}
          # 当前提交哈希，作为扫描的目标版本
          head: ${{ github.event.pull_request.head.sha }}

  # 构建和测试 job（并行执行）
  build-and-test:
    name: Build and Test
    runs-on: ubuntu-latest

    steps:
      # 检出源代码
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
    
      # 安装和配置.NET SDK版本
      - name: Setup .NET 8
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: 8.0.x
    
      - name: Setup .NET 9
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: 9.0.x
    
      - name: Setup .NET 10
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: 10.0.x

      # 配置NuGet包缓存以加速后续构建
      - name: Cache NuGet packages
        uses: actions/cache@v5
        with:
          path: |
            ~/.nuget/packages
            ~/.local/share/NuGet
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/nuget.config') }}
      
      # 配置.NET本地工具缓存以加速后续构建
      - name: Cache dotnet tools
        uses: actions/cache@v5
        with:
          path: ~/.dotnet/tools
          key: ${{ runner.os }}-dotnet-tools-${{ hashFiles('.config/dotnet-tools.json') }}

      # 执行NuGet包恢复操作
      - name: Restore
        run: dotnet restore GFramework.sln
      # 恢复.NET本地工具
      - name: Restore .NET tools
        run: dotnet tool restore

      - name: Setup Node.js 20
        uses: actions/setup-node@v6
        with:
          node-version: 20

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: 1.2.15

      - name: Install config tool dependencies
        working-directory: tools/gframework-config-tool
        run: bun install

      - name: Run config tool tests
        working-directory: tools/gframework-config-tool
        run: bun run test
        
      # 构建项目，使用Release配置且跳过恢复步骤
      - name: Build
        run: dotnet build GFramework.sln -c Release --no-restore

      # 运行单元测试，输出TRX格式结果到TestResults目录
      # 顺序执行各测试项目，避免并发 dotnet test 进程导致“TRX 全绿但 step 仍返回失败”的假红状态
      - name: Test All Projects
        id: test_all_projects
        run: |
          set -euo pipefail
          mkdir -p TestResults

          test_projects=(
            "GFramework.Core.Tests/GFramework.Core.Tests.csproj:core"
            "GFramework.Game.Tests/GFramework.Game.Tests.csproj:game"
            "GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj:sg"
            "GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj:cqrs"
            "GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj:ecs-arch"
            "GFramework.Godot.Tests/GFramework.Godot.Tests.csproj:godot"
            "GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj:godot-sg"
          )

          failed=0
          failed_projects=()
          failed_log_paths=()

          for entry in "${test_projects[@]}"; do
            project="${entry%%:*}"
            name="${entry##*:}"
            log_path="TestResults/${name}.console.log"

            echo "::group::dotnet test $project"
            if ! dotnet test "$project" \
              -c Release \
              --no-build \
              --logger "trx;LogFileName=${name}.trx" \
              --results-directory TestResults \
              2>&1 | tee "$log_path"; then
              failed=1
              failed_projects+=("$project")
              failed_log_paths+=("$log_path")
              echo "::error title=Test project failed::$project returned a non-zero exit code."
            fi
            echo "::endgroup::"
          done

          if [ "$failed" -eq 1 ]; then
            printf 'Failed test projects:\n'
            printf '  %s\n' "${failed_projects[@]}"
          fi

          {
            echo "failed=$failed"
            echo "failed_projects<<EOF"
            if [ "${#failed_projects[@]}" -gt 0 ]; then
              printf '%s\n' "${failed_projects[@]}"
            fi
            echo "EOF"
            echo "failed_log_paths<<EOF"
            if [ "${#failed_log_paths[@]}" -gt 0 ]; then
              printf '%s\n' "${failed_log_paths[@]}"
            fi
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Generate CTRF report
        run: |
          mkdir -p ctrf

          for trx in TestResults/*.trx; do
            name=$(basename "$trx" .trx)
            echo "Processing $trx -> ctrf/$name.json"

            dotnet tool run DotnetCtrfJsonReporter \
              -p "$trx" \
              -t nunit \
              -d ctrf \
              -f "$name.json"
          done

      - name: Run GFramework.Godot.Tests Diagnostics
        if: always() && contains(steps.test_all_projects.outputs.failed_projects, 'GFramework.Godot.Tests/GFramework.Godot.Tests.csproj')
        continue-on-error: true
        run: |
          mkdir -p TestResults
          dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj \
            -c Release \
            --no-build \
            --blame-crash \
            --diag TestResults/godot-testhost-diag.log \
            --logger "trx;LogFileName=godot-diagnostic.trx" \
            --results-directory TestResults \
            2>&1 | tee TestResults/godot-diagnostic.console.log
      
      
      # 生成并发布测试报告，无论测试成功或失败都会执行
      - name: Test Report
        uses: dorny/test-reporter@v3
        if: always()
        with:
          name: .NET Test Results
          path: TestResults/*.trx
          reporter: dotnet-trx
      - name: Publish Test Report
        uses: ctrf-io/github-test-reporter@v1
        with:
          report-path: './ctrf/*.json'
          github-report: true
          pull-request-report: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
          summary-delta-report: true
          insights-report: true
          flaky-rate-report: true
          fail-rate-report: true
          slowest-report: true
          upload-artifact: true
          fetch-previous-results: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        if: always()

      - name: Fail if any test project failed
        if: always() && steps.test_all_projects.outputs.failed == '1'
        env:
          FAILED_PROJECTS: ${{ steps.test_all_projects.outputs.failed_projects }}
          FAILED_LOG_PATHS: ${{ steps.test_all_projects.outputs.failed_log_paths }}
        run: |
          echo "The following test projects returned non-zero exit codes:"
          printf '%s\n' "$FAILED_PROJECTS"
          echo
          echo "Captured dotnet test output:"
          while IFS= read -r log_path; do
            if [ -n "$log_path" ] && [ -f "$log_path" ]; then
              echo "--- BEGIN $log_path ---"
              cat "$log_path"
              echo "--- END $log_path ---"
            fi
          done <<< "$FAILED_LOG_PATHS"
          exit 1
