// tests/kb-update/test-atomic-write.test.mjs
// Unit tests for scripts/kb-update/lib/atomic-write.mjs

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, readFileSync, existsSync, readdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { atomicWriteSync, atomicWriteJson } from '../../scripts/kb-update/lib/atomic-write.mjs';

async function withTmp(fn) {
  const dir = mkdtempSync(join(tmpdir(), 'aw-test-'));
  try {
    return await fn(dir);
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
}

test('atomicWriteSync — writes string content to target', () => {
  withTmp((dir) => {
    const path = join(dir, 'state.txt');
    atomicWriteSync(path, 'hello');
    assert.equal(readFileSync(path, 'utf8'), 'hello');
  });
});

test('atomicWriteSync — leaves no .tmp orphan after success', () => {
  withTmp((dir) => {
    const path = join(dir, 'state.txt');
    atomicWriteSync(path, 'ok');
    const orphans = readdirSync(dir).filter((f) => f.includes('.tmp.'));
    assert.deepEqual(orphans, []);
  });
});

test('atomicWriteSync — overwrites existing file', () => {
  withTmp((dir) => {
    const path = join(dir, 'state.txt');
    writeFileSync(path, 'old');
    atomicWriteSync(path, 'new');
    assert.equal(readFileSync(path, 'utf8'), 'new');
  });
});

test('atomicWriteSync — accepts Buffer content', () => {
  withTmp((dir) => {
    const path = join(dir, 'bin');
    atomicWriteSync(path, Buffer.from([0x01, 0x02, 0x03]));
    const data = readFileSync(path);
    assert.equal(data.length, 3);
    assert.equal(data[0], 0x01);
  });
});

test('atomicWriteJson — round-trips object', () => {
  withTmp((dir) => {
    const path = join(dir, 'state.json');
    const obj = { schema_version: 1, status: 'success', items: [1, 2, 3] };
    atomicWriteJson(path, obj);
    const read = JSON.parse(readFileSync(path, 'utf8'));
    assert.deepEqual(read, obj);
  });
});

test('atomicWriteJson — pretty-prints with 2-space indent', () => {
  withTmp((dir) => {
    const path = join(dir, 'state.json');
    atomicWriteJson(path, { a: 1, b: { c: 2 } });
    const text = readFileSync(path, 'utf8');
    assert.match(text, /\n {2}"a": 1/);
    assert.match(text, /\n {4}"c": 2/);
  });
});

test('atomicWriteJson — leaves no .tmp orphan', () => {
  withTmp((dir) => {
    const path = join(dir, 'state.json');
    atomicWriteJson(path, { ok: true });
    const orphans = readdirSync(dir).filter((f) => f.includes('.tmp.'));
    assert.deepEqual(orphans, []);
  });
});

test('atomicWriteSync — concurrent writes do not corrupt', async () => {
  await withTmp(async (dir) => {
    const path = join(dir, 'concurrent.json');
    const writes = [];
    for (let i = 0; i < 50; i++) {
      writes.push(
        Promise.resolve().then(() => atomicWriteJson(path, { iter: i, payload: 'x'.repeat(100) }))
      );
    }
    await Promise.all(writes);
    // Final read must be valid JSON, regardless of which write won.
    const text = readFileSync(path, 'utf8');
    const obj = JSON.parse(text);
    assert.equal(typeof obj.iter, 'number');
    assert.ok(obj.iter >= 0 && obj.iter < 50);
    assert.equal(obj.payload.length, 100);
    // No .tmp orphans
    const orphans = readdirSync(dir).filter((f) => f.includes('.tmp.'));
    assert.deepEqual(orphans, []);
  });
});

test('atomicWriteSync — tmp filename uses pid + random suffix', () => {
  // Indirect verification: write then immediately check the only file in dir is the target.
  withTmp((dir) => {
    const path = join(dir, 'target.json');
    atomicWriteSync(path, '{}');
    const files = readdirSync(dir);
    assert.deepEqual(files, ['target.json']);
  });
});
