import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert/strict';

// main() 是整個 action 的流程總管（Step1~Step11），本測試用 node:test 的 module mock
// 把所有相依模組換成可控 stub，逐一驗證關鍵分支的 exit code 與退出時機。
// 需要 `--experimental-test-module-mocks`（見 package.json test script）。

// 略過 main.js 被 import 時的自動執行；本測試改為手動呼叫 export 的 main()。
process.env.AI_REVIEW_SKIP_MAIN = '1';

const U = (rel) => new URL(rel, import.meta.url).href;

// 每個相依模組的預設 stub（快樂路徑：無 critical、diff 非空、角色分析成功）。
// 各測試以 overrides 覆寫要驗證的分支。
function baseStubs() {
  return {
    config: {
      GITEA_REPOSITORY: 'owner/repo', PR_NUMBER: '1', PR_HEAD_BRANCH: 'feat', PR_BASE_BRANCH: 'develop',
      FINDINGS_PATH: '.gitea/ai-review/findings.json', EXCLUSIONS_PATH: '.gitea/ai-review/exclusions.json',
      getLLMConfig: () => ({ provider: 'codex', apiKeys: ['codex'], baseURL: null, model: 'gpt-5.5', command: 'codex' }),
    },
    roles: { loadRoles: () => [{ name: 'Mage' }], getRoleIntro: () => 'intro' },
    gitea: {
      getPRDiff: async () => 'diff --git a/x b/x\n+code',
      postComment: async () => ({ id: 1 }),
      getCommitMessageBySha: async () => 'normal commit',
      getBotReviewOutcome: () => null,
      shouldSkipBotCommit: async () => false,
    },
    findings: {
      analyzeWithRole: async () => [],
      loadOldFindings: () => [],
      mergeFindings: (a, b) => [...a, ...b],
      sortByLevel: (a) => a,
      deduplicateWithAI: async (a) => a,
      loadExclusions: () => [],
      applyExclusions: (a) => a,
      filterFalsePositivesWithAI: async (a) => a,
      appendExclusions: () => null,
      resolveMissingLineNumbers: async (a) => a,
    },
    resolve: {
      reconcileConversations: async () => ({ resolvedFindings: [], excludedFindings: [], carriedFindings: [], resolvedCount: 0, falsePositiveCount: 0, openCount: 0, closedCount: 0 }),
      dropResolvedFindings: (a) => a,
      addCarriedFindings: (a) => a,
    },
    comments: { saveFindings: () => {}, postFindingsReview: async () => {}, formatFindingsStatsLine: () => '' },
    usage: { getRunUsage: () => ({}), getRateLimit: () => ({}), fetchAccountQuota: async () => ({}), formatUsageStats: () => '', formatUsageStatsLine: () => '' },
    git: { cloneRepo: () => '/repo', commitAndPush: async () => {}, getRepoState: () => ({ branch: 'feat', shortSha: 'abc' }) },
    json: { validateJSONArrayFile: async () => ({ exists: true }), ensureJSONArrayFileExists: () => {} },
    preflight: { runPreflight: async () => true },
    llm: { mapWithConcurrency: async (items, _l, fn) => Promise.all(items.map(fn)), LLM_CONCURRENCY: 0 },
    log: { section() {}, step() {}, line() {}, input() {}, output() {}, result() {}, warn() {}, error() {} },
  };
}

// 套用 mock 並載入一份全新的 main（以 query 破快取），回傳 main()。
let importSeq = 0;
async function loadMain(overrides = {}) {
  const s = baseStubs();
  for (const k of Object.keys(overrides)) s[k] = { ...s[k], ...overrides[k] };
  mock.module(U('../config.js'), { namedExports: s.config });
  mock.module(U('../roles.js'), { namedExports: s.roles });
  mock.module(U('../gitea.js'), { namedExports: s.gitea });
  mock.module(U('../findings.js'), { namedExports: s.findings });
  mock.module(U('../resolve.js'), { namedExports: s.resolve });
  mock.module(U('../comments.js'), { namedExports: s.comments });
  mock.module(U('../usage.js'), { namedExports: s.usage });
  mock.module(U('../git.js'), { namedExports: s.git });
  mock.module(U('../json.js'), { namedExports: s.json });
  mock.module(U('../preflight.js'), { namedExports: s.preflight });
  mock.module(U('../llm.js'), { namedExports: s.llm });
  mock.module(U('../log.js'), { namedExports: s.log });
  const mod = await import(`../main.js?seq=${importSeq++}`);
  return mod.main;
}

// 執行 main()，攔截 process.exit 並回傳 exit code（正常結束回 0）。
async function runMain(overrides) {
  const main = await loadMain(overrides);
  mock.method(process, 'exit', (code) => { throw Object.assign(new Error('__exit__'), { __code: code ?? 0 }); });
  try {
    await main();
    return 0;
  } catch (e) {
    if (e && e.__code !== undefined) return e.__code;
    throw e;
  }
}

describe('main pipeline', () => {
  beforeEach(() => { process.env.PR_HEAD_SHA = 'deadbeef'; });
  afterEach(() => { mock.restoreAll(); delete process.env.PR_HEAD_SHA; });

  it('前置驗證失敗 → exit 1', async () => {
    assert.equal(await runMain({ preflight: { runPreflight: async () => false } }), 1);
  });

  it('偵測到 [ai-review-bot][failure] → exit 1', async () => {
    assert.equal(await runMain({
      gitea: { getCommitMessageBySha: async () => 'chore: update [ai-review-bot][failure]', getBotReviewOutcome: () => 'failure' },
    }), 1);
  });

  it('本次為 bot 自動提交 → exit 0（跳過審查）', async () => {
    assert.equal(await runMain({ gitea: { shouldSkipBotCommit: async () => true } }), 0);
  });

  it('未偵測到 LLM provider → exit 1', async () => {
    assert.equal(await runMain({ config: { getLLMConfig: () => ({ provider: null, apiKeys: [], baseURL: null, model: null, command: null }) } }), 1);
  });

  it('diff 為空 → exit 0', async () => {
    assert.equal(await runMain({ gitea: { getPRDiff: async () => '   ' } }), 0);
  });

  it('所有角色分析皆失敗 → exit 1', async () => {
    assert.equal(await runMain({ findings: { analyzeWithRole: async () => { throw new Error('boom'); } } }), 1);
  });

  it('JSON 格式驗證失敗 → exit 1', async () => {
    assert.equal(await runMain({ json: { validateJSONArrayFile: async () => { throw new Error('bad json'); } } }), 1);
  });

  it('產生 critical finding → exit 1', async () => {
    const crit = [{ level: 'critical', role: 'Mage', location: 'a.js:1', suggestion: 'fix' }];
    assert.equal(await runMain({ findings: { analyzeWithRole: async () => crit, filterFalsePositivesWithAI: async () => crit } }), 1);
  });

  it('無 critical → 正常走完（exit 0）', async () => {
    assert.equal(await runMain(), 0);
  });

  it('clone 失敗仍繼續、不因 commitAndPush 中斷（無 critical → exit 0）', async () => {
    assert.equal(await runMain({ git: { cloneRepo: () => { throw new Error('clone fail'); } } }), 0);
  });
});
