/**
 * Integration test: WAL auto-checkpoint rename failure (#1741 / #1772).
 *
 * Drives the real `analyzeCommand` against a real LadybugDB instance and
 * provokes a genuine Ladybug-engine `IO exception: Error renaming file
 * <db>.wal to <db>.wal.checkpoint` by pre-planting a *directory* at the
 * `<db>.wal.checkpoint` rename target. `fs.rename` (which Ladybug's native
 * `LocalFileSystem` ultimately invokes) cannot overwrite a non-empty
 * directory with a file on either POSIX or Windows, and Ladybug's
 * `doInitLbug` orphan-cleanup uses `fs.unlink` which fails on a directory
 * — so the blocker survives initialization and the next auto-checkpoint
 * fires the natural rename failure that motivated PR #1772.
 *
 * No test-only hooks, no env-var fault toggles in production code: we use
 * the same `GITNEXUS_WAL_CHECKPOINT_THRESHOLD=1` knob that real users have
 * available to force checkpointing on every write, then arrange a real
 * filesystem state that makes the rename impossible.
 *
 * Verifies that:
 *   1. The CLI exits non-zero.
 *   2. stderr contains the actionable recovery hint pointing at
 *      `--wal-checkpoint-threshold 67108864` (the
 *      `RECOMMENDED_WAL_CHECKPOINT_THRESHOLD` constant in `analyze.ts`).
 *   3. The recovery message references the
 *      `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` env var as a parallel route.
 *
 * Empirically confirmed portable on Windows; the same mechanism is
 * expected to work on POSIX (`rename(2)` fails with `EISDIR`/`ENOTEMPTY`
 * when the target is a non-empty directory). If a future Ladybug release
 * changes the rename ordering, the loose match on the recovery hint
 * (rather than the exact engine error wording) keeps this test stable.
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { CLI_SPAWN_PREFIX } from '../helpers/cli-entry.js';
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { fileURLToPath } from 'url';
import { cleanupTempDirSync } from '../helpers/test-db.js';

const testDir = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE_SRC = path.resolve(testDir, '..', 'fixtures', 'mini-repo');

let tmpParent: string;
let suiteGitnexusHome: string;
let repoPath: string;

beforeAll(() => {
  tmpParent = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-wal-checkpoint-e2e-'));
  suiteGitnexusHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-wal-checkpoint-home-'));
  repoPath = path.join(tmpParent, 'mini-repo');
  fs.cpSync(FIXTURE_SRC, repoPath, { recursive: true });

  spawnSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' });
  spawnSync('git', ['add', '-A'], { cwd: repoPath, stdio: 'pipe' });
  spawnSync('git', ['commit', '-m', 'initial commit'], {
    cwd: repoPath,
    stdio: 'pipe',
    env: {
      ...process.env,
      GIT_AUTHOR_NAME: 'test',
      GIT_AUTHOR_EMAIL: 'test@test',
      GIT_COMMITTER_NAME: 'test',
      GIT_COMMITTER_EMAIL: 'test@test',
    },
  });
});

afterAll(() => {
  if (tmpParent) cleanupTempDirSync(tmpParent);
  if (suiteGitnexusHome) cleanupTempDirSync(suiteGitnexusHome);
});

const runAnalyze = () =>
  spawnSync(process.execPath, [...CLI_SPAWN_PREFIX, 'analyze', '--skip-skills'], {
    cwd: repoPath,
    encoding: 'utf8',
    // Generous timeout: the test does real CSV/COPY work before the
    // first failing checkpoint, and CI runners are slow.
    timeout: process.env.CI ? 120_000 : 60_000,
    stdio: ['pipe', 'pipe', 'pipe'],
    env: {
      ...process.env,
      GITNEXUS_HOME: suiteGitnexusHome,
      // Skip ensureHeap re-exec (which drops the tsx loader).
      NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --max-old-space-size=8192`.trim(),
      // Tiny threshold forces auto-checkpoint on every write so the
      // first write into the WAL trips the planted rename blocker.
      GITNEXUS_WAL_CHECKPOINT_THRESHOLD: '1',
      CI: '1',
    },
  });

describe('analyze WAL auto-checkpoint rename failure (real lbug, no mocks)', () => {
  it('surfaces the --wal-checkpoint-threshold recovery hint when the rename target is blocked', () => {
    // The checkpoint rename target must be a PREDICTABLE path so the blocker
    // can be pre-planted. A full rebuild builds into a per-run
    // `lbug.staging.<uuid>` and checkpoints `lbug.staging.<uuid>.wal.checkpoint`
    // (#2658) — an unknowable name. An INCREMENTAL run instead writes the live
    // index in place, so its auto-checkpoint targets the fixed
    // `lbug.wal.checkpoint`. So: first do a clean full analyze to create the
    // index, then plant the blocker and drive an incremental analyze into it.
    const storageDir = path.join(repoPath, '.gitnexus');

    // 1) Clean full analyze (no blocker) — builds the index into staging and
    // swaps it in. Must succeed; the staging checkpoint name is unblocked.
    const first = runAnalyze();
    expect(first.status === null ? 'timeout' : first.status).toBe(0);

    // 2) Change a tracked source file and commit, so the next analyze is an
    // incremental writeback (in-place), not a full rebuild.
    const churnFile = path.join(repoPath, 'src', 'logger.ts');
    fs.appendFileSync(churnFile, `\nexport const walChurnMarker = ${Date.now()};\n`);
    const gitEnv = {
      ...process.env,
      GIT_AUTHOR_NAME: 'test',
      GIT_AUTHOR_EMAIL: 'test@test',
      GIT_COMMITTER_NAME: 'test',
      GIT_COMMITTER_EMAIL: 'test@test',
    };
    spawnSync('git', ['add', '-A'], { cwd: repoPath, stdio: 'pipe' });
    spawnSync('git', ['commit', '-m', 'churn for incremental'], {
      cwd: repoPath,
      stdio: 'pipe',
      env: gitEnv,
    });

    // 3) Plant a non-empty directory at `lbug.wal.checkpoint`, the fixed rename
    // target of the in-place checkpoint. `fs.rename` cannot overwrite a
    // non-empty directory, and the adapter's orphan-sidecar cleanup uses
    // `fs.unlink` (which fails on a directory) — so the blocker persists through
    // `doInitLbug` and trips the auto-checkpoint the incremental writeback
    // forces at `GITNEXUS_WAL_CHECKPOINT_THRESHOLD=1`.
    const blockerDir = path.join(storageDir, 'lbug.wal.checkpoint');
    fs.rmSync(blockerDir, { recursive: true, force: true });
    fs.mkdirSync(blockerDir, { recursive: true });
    fs.writeFileSync(path.join(blockerDir, 'blocker'), 'cannot-be-renamed-over');

    // 4) Incremental analyze into the blocked checkpoint target.
    const result = runAnalyze();
    const combined = `${result.stderr}\n${result.stdout}`;

    // The CLI must exit non-zero. status === null means the timeout fired
    // without a clean exit — also a failure for this assertion.
    expect(result.status === null ? 'timeout' : result.status).not.toBe(0);

    // Recovery hint must reference the CLI flag and the recommended
    // 64 MiB threshold (67_108_864 bytes). Both come from the
    // RECOMMENDED_WAL_CHECKPOINT_THRESHOLD constant in analyze.ts; keep
    // those values in sync with this assertion if the constant changes.
    expect(combined).toContain('gitnexus analyze --wal-checkpoint-threshold');
    expect(combined).toContain('67108864');
    // The env-var route should be advertised alongside the flag.
    expect(combined).toContain('GITNEXUS_WAL_CHECKPOINT_THRESHOLD');
  }, 180_000);
});
