import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { recordVersion, resolveWriteTarget } from './conflict-guard.js';

describe('conflict-guard', () => {
  let ws: string;
  let conflictDir: string;
  beforeEach(() => {
    ws = mkdtempSync(join(tmpdir(), 'cg-'));
    // 計画5: 台帳は files ツリーの外（スペース単位 .conflict/）に住む。
    conflictDir = join(ws, '.conflict');
  });
  afterEach(() => { rmSync(ws, { recursive: true, force: true }); });

  it('recordVersion is best-effort: a directory / missing path never throws and records nothing', () => {
    mkdirSync(join(ws, 'adir'), { recursive: true });
    writeFileSync(join(ws, 'real.txt'), 'hello');
    recordVersion(ws, 'real.txt', conflictDir); // sanity: a real file IS recorded
    // Regression: readFileSync(dir) → EISDIR previously propagated and crashed
    // the job. recordVersion must swallow it (and record nothing for a dir).
    expect(() => recordVersion(ws, 'adir', conflictDir)).not.toThrow();
    expect(() => recordVersion(ws, 'does-not-exist.txt', conflictDir)).not.toThrow();
    const ledger = JSON.parse(readFileSync(join(conflictDir, 'file-versions.json'), 'utf-8'));
    expect(ledger).toHaveProperty('real.txt');          // real file recorded
    expect(ledger).not.toHaveProperty('adir');          // directory NOT recorded
    expect(ledger).not.toHaveProperty('does-not-exist.txt');
  });

  it('a brand-new file writes to its own path (no conflict)', () => {
    const r = resolveWriteTarget(ws, 'output/a.txt', 'hello', conflictDir);
    expect(r.conflict).toBe(false);
    expect(r.targetRelPath).toBe('output/a.txt');
  });

  it('overwriting a file whose disk content matches the recorded version is NOT a conflict', () => {
    writeFileSync(join(ws, 'a.txt'), 'v1');
    recordVersion(ws, 'a.txt', conflictDir); // agent read/wrote it, base = v1
    const r = resolveWriteTarget(ws, 'a.txt', 'v2', conflictDir);
    expect(r.conflict).toBe(false);
    expect(r.targetRelPath).toBe('a.txt');
  });

  it('overwriting a file changed on disk since the recorded version → archives old file', () => {
    writeFileSync(join(ws, 'a.txt'), 'v1');
    recordVersion(ws, 'a.txt', conflictDir);      // base = v1
    writeFileSync(join(ws, 'a.txt'), 'EXTERNAL');  // someone else changed it
    const r = resolveWriteTarget(ws, 'a.txt', 'v2', conflictDir);
    expect(r.conflict).toBe(true);
    expect(r.targetRelPath).toBe('a.txt');
    expect(r.archivedRelPath).toBe('old/a_old1.txt');
    expect(readFileSync(join(ws, 'old', 'a_old1.txt'), 'utf-8')).toBe('EXTERNAL');
    expect(existsSync(join(ws, 'a.txt'))).toBe(false);
  });

  it('overwriting an existing file with NO recorded base (never read by this agent) → archives old file', () => {
    writeFileSync(join(ws, 'a.txt'), 'pre-existing');
    const r = resolveWriteTarget(ws, 'a.txt', 'v2', conflictDir);
    expect(r.conflict).toBe(true); // blind overwrite of unknown content is treated as conflict
    expect(r.targetRelPath).toBe('a.txt');
    expect(r.archivedRelPath).toBe('old/a_old1.txt');
  });

  it('the ledger is created under the given conflictDir, NOT under files/ or logs/', () => {
    writeFileSync(join(ws, 'a.txt'), 'v1');
    recordVersion(ws, 'a.txt', conflictDir);
    expect(existsSync(join(conflictDir, 'file-versions.json'))).toBe(true);
    // 旧 logs/.file-versions.json には書かない。
    expect(existsSync(join(ws, 'logs', '.file-versions.json'))).toBe(false);
    expect(existsSync(join(ws, '.file-versions.json'))).toBe(false);
  });

  it('files under logs/ are now versioned normally (exclusion removed)', () => {
    // 台帳が files の外に出たので logs/ を特別扱いしない。
    writeFileSync(join(ws, 'logs.txt'), 'a log');
    recordVersion(ws, 'logs.txt', conflictDir);
    const led = JSON.parse(readFileSync(join(conflictDir, 'file-versions.json'), 'utf-8'));
    expect(led['logs.txt']).toBeTruthy();
  });

  it('sequential recordVersion calls do not drop keys (no last-writer-wins)', () => {
    for (let i = 0; i < 25; i++) {
      writeFileSync(join(ws, `f${i}.txt`), `content-${i}`);
      recordVersion(ws, `f${i}.txt`, conflictDir);
    }
    const led = JSON.parse(readFileSync(join(conflictDir, 'file-versions.json'), 'utf-8'));
    expect(Object.keys(led)).toHaveLength(25);
    for (let i = 0; i < 25; i++) expect(led[`f${i}.txt`]).toBeTruthy();
  });

  it('old archive names do not overwrite existing old files', () => {
    mkdirSync(join(ws, 'old'), { recursive: true });
    writeFileSync(join(ws, 'old', 'a_old1.txt'), 'previous-old');
    writeFileSync(join(ws, 'a.txt'), 'pre-existing');
    const r = resolveWriteTarget(ws, 'a.txt', 'v2', conflictDir);
    expect(r.conflict).toBe(true);
    expect(r.targetRelPath).toBe('a.txt');
    expect(r.archivedRelPath).toBe('old/a_old2.txt');
    expect(readFileSync(join(ws, 'old', 'a_old1.txt'), 'utf-8')).toBe('previous-old');
    expect(readFileSync(join(ws, 'old', 'a_old2.txt'), 'utf-8')).toBe('pre-existing');
  });

  it('archives old files into old/ beside nested targets', () => {
    mkdirSync(join(ws, 'output'), { recursive: true });
    writeFileSync(join(ws, 'output', 'report.md'), 'old report');
    const r = resolveWriteTarget(ws, 'output/report.md', 'new report', conflictDir);
    expect(r.conflict).toBe(true);
    expect(r.targetRelPath).toBe('output/report.md');
    expect(r.archivedRelPath).toBe('output/old/report_old1.md');
    expect(readFileSync(join(ws, 'output', 'old', 'report_old1.md'), 'utf-8')).toBe('old report');
  });
});
