import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { writeAtomicJson, readSafeJson, quarantineCorruptFile, type AtomicJsonSchema } from './atomic-json.js';

interface TestPayload {
  version: 1;
  hello: string;
  count: number;
}

const TEST_SCHEMA: AtomicJsonSchema<TestPayload> = {
  expectedVersion: 1,
  validate: (p): string | null => {
    const obj = p as Record<string, unknown>;
    if (typeof obj.hello !== 'string') return 'hello must be string';
    if (typeof obj.count !== 'number') return 'count must be number';
    return null;
  },
  cast: (p): TestPayload => p as TestPayload,
};

describe('writeAtomicJson + readSafeJson', () => {
  let dir: string;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'atomic-json-test-'));
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  it('round-trips a valid payload', () => {
    const path = join(dir, 'data.json');
    const payload: TestPayload = { version: 1, hello: 'world', count: 7 };
    writeAtomicJson(path, payload);

    const result = readSafeJson(path, TEST_SCHEMA);
    expect(result.kind).toBe('ok');
    if (result.kind === 'ok') {
      expect(result.value.hello).toBe('world');
      expect(result.value.count).toBe(7);
    }
  });

  it('creates parent directories on write', () => {
    const path = join(dir, 'nested', 'sub', 'data.json');
    writeAtomicJson(path, { version: 1, hello: 'x', count: 0 });
    expect(existsSync(path)).toBe(true);
  });

  it('leaves no .tmp files behind on success', () => {
    const path = join(dir, 'data.json');
    writeAtomicJson(path, { version: 1, hello: 'x', count: 0 });
    const leftover = readdirSync(dir).filter((f) => f.includes('.tmp.'));
    expect(leftover).toEqual([]);
  });

  it('returns missing for nonexistent file', () => {
    const result = readSafeJson(join(dir, 'absent.json'), TEST_SCHEMA);
    expect(result.kind).toBe('missing');
  });

  it('returns corrupt for unparseable JSON', () => {
    const path = join(dir, 'bad.json');
    writeFileSync(path, '{not valid json', 'utf-8');
    const result = readSafeJson(path, TEST_SCHEMA);
    expect(result.kind).toBe('corrupt');
    if (result.kind === 'corrupt') {
      expect(result.reason).toMatch(/JSON parse/);
    }
  });

  it('returns corrupt for wrong version', () => {
    const path = join(dir, 'v2.json');
    writeFileSync(path, JSON.stringify({ version: 2, hello: 'x', count: 0 }), 'utf-8');
    const result = readSafeJson(path, TEST_SCHEMA);
    expect(result.kind).toBe('corrupt');
    if (result.kind === 'corrupt') {
      expect(result.reason).toMatch(/version mismatch/);
    }
  });

  it('returns corrupt for missing required fields', () => {
    const path = join(dir, 'incomplete.json');
    writeFileSync(path, JSON.stringify({ version: 1, hello: 'x' }), 'utf-8');
    const result = readSafeJson(path, TEST_SCHEMA);
    expect(result.kind).toBe('corrupt');
    if (result.kind === 'corrupt') {
      expect(result.reason).toMatch(/count must be number/);
    }
  });

  it('returns corrupt for non-object top level', () => {
    const path = join(dir, 'array.json');
    writeFileSync(path, JSON.stringify([1, 2, 3]), 'utf-8');
    const result = readSafeJson(path, TEST_SCHEMA);
    expect(result.kind).toBe('corrupt');
  });

  it('overwrites existing files atomically', () => {
    const path = join(dir, 'data.json');
    writeAtomicJson(path, { version: 1, hello: 'first', count: 1 });
    writeAtomicJson(path, { version: 1, hello: 'second', count: 2 });
    const result = readSafeJson(path, TEST_SCHEMA);
    expect(result.kind).toBe('ok');
    if (result.kind === 'ok') {
      expect(result.value.hello).toBe('second');
    }
  });
});

describe('quarantineCorruptFile', () => {
  let dir: string;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'atomic-json-quarantine-'));
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  it('renames a corrupt file aside so it is not read on next run', () => {
    const path = join(dir, 'log.json');
    writeFileSync(path, '{not json', 'utf-8');
    const moved = quarantineCorruptFile(path);
    expect(moved).not.toBeNull();
    expect(existsSync(path)).toBe(false);
    expect(moved && existsSync(moved)).toBe(true);
    expect(moved).toMatch(/log\.json\.corrupt\..*\.json$/);
    // Original content is preserved in quarantine for forensics.
    expect(readFileSync(moved!, 'utf-8')).toBe('{not json');
  });

  it('returns null when target does not exist', () => {
    const moved = quarantineCorruptFile(join(dir, 'absent.json'));
    expect(moved).toBeNull();
  });
});
