import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import {
  loadHashManifest,
  saveHashManifest,
  reconcileWrites,
  sha256,
  HASHES_DIR,
  HASHES_FILE,
} from '../regen-safety.js';

let projectRoot: string;

beforeEach(() => {
  projectRoot = mkdtempSync(join(tmpdir(), 'specverse-regen-safety-'));
});

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

describe('hash manifest persistence', () => {
  it('loadHashManifest returns {} when the file is missing', () => {
    expect(loadHashManifest(projectRoot)).toEqual({});
  });

  it('round-trips through save → load', () => {
    saveHashManifest(projectRoot, { 'src/a.ts': 'abc', 'src/b.ts': 'def' });
    const loaded = loadHashManifest(projectRoot);
    expect(loaded).toEqual({ 'src/a.ts': 'abc', 'src/b.ts': 'def' });
    expect(existsSync(join(projectRoot, HASHES_DIR, HASHES_FILE))).toBe(true);
  });

  it('tolerates a malformed manifest file by returning {}', () => {
    const dir = join(projectRoot, HASHES_DIR);
    mkdirSync(dir, { recursive: true });
    writeFileSync(join(dir, HASHES_FILE), '{ not valid JSON', 'utf8');
    expect(loadHashManifest(projectRoot)).toEqual({});
  });
});

describe('reconcileWrites — pure planning (no I/O writes)', () => {
  it('approves brand-new files and records their hashes', () => {
    const proposed = { 'src/views/PostListView.tsx': 'content-a' };
    const result = reconcileWrites(projectRoot, proposed, {});
    expect(result.approvedWrites).toEqual(proposed);
    expect(result.skipped).toEqual([]);
    expect(existsSync(join(projectRoot, 'src/views/PostListView.tsx'))).toBe(false);
    expect(result.manifest['src/views/PostListView.tsx']).toBe(sha256('content-a'));
  });

  it('approves overwriting a pristine file and updates its hash', () => {
    const path = 'src/a.ts';
    const absPath = join(projectRoot, path);
    const originalContent = 'v1';
    const newContent = 'v2';

    mkdirSync(join(projectRoot, 'src'), { recursive: true });
    writeFileSync(absPath, originalContent, 'utf8');
    const prevManifest = { [path]: sha256(originalContent) };

    const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);

    expect(result.approvedWrites[path]).toBe(newContent);
    expect(result.skipped).toEqual([]);
    // reconcileWrites is pure planning — filesystem unchanged
    expect(readFileSync(absPath, 'utf8')).toBe(originalContent);
    expect(result.manifest[path]).toBe(sha256(newContent));
  });

  it('skips a user-edited file and keeps the old hash record', () => {
    const path = 'src/edited.ts';
    const absPath = join(projectRoot, path);
    const originalContent = 'v1';
    const editedContent = 'v1 + user edits';
    const newContent = 'v2';

    mkdirSync(join(projectRoot, 'src'), { recursive: true });
    writeFileSync(absPath, editedContent, 'utf8');
    const prevManifest = { [path]: sha256(originalContent) };

    const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);

    expect(result.approvedWrites[path]).toBeUndefined();
    expect(result.skipped).toHaveLength(1);
    expect(result.skipped[0]).toMatchObject({
      path,
      reason: expect.stringContaining('edited'),
    });
    expect(readFileSync(absPath, 'utf8')).toBe(editedContent);
    expect(result.manifest[path]).toBe(prevManifest[path]);
  });

  it('skips files that exist but have no prior hash record', () => {
    const path = 'src/unknown.ts';
    const absPath = join(projectRoot, path);

    mkdirSync(join(projectRoot, 'src'), { recursive: true });
    writeFileSync(absPath, 'manually placed', 'utf8');

    const result = reconcileWrites(projectRoot, { [path]: 'would-overwrite' }, {});

    expect(result.approvedWrites[path]).toBeUndefined();
    expect(result.skipped).toHaveLength(1);
    expect(result.skipped[0].reason).toContain('no prior hash recorded');
    expect(readFileSync(absPath, 'utf8')).toBe('manually placed');
  });

  it('does not mutate the input prevManifest', () => {
    const prev = { 'a.ts': sha256('old') };
    const prevSnapshot = { ...prev };
    reconcileWrites(projectRoot, { 'a.ts': 'new', 'b.ts': 'also new' }, prev);
    expect(prev).toEqual(prevSnapshot);
  });

  it('handles a batch of mixed outcomes correctly', () => {
    mkdirSync(join(projectRoot, 'src'), { recursive: true });
    writeFileSync(join(projectRoot, 'src/pristine.ts'), 'v1', 'utf8');
    writeFileSync(join(projectRoot, 'src/edited.ts'), 'v1 + user edit', 'utf8');
    const prev = {
      'src/pristine.ts': sha256('v1'),
      'src/edited.ts': sha256('v1'),
    };

    const proposed = {
      'src/pristine.ts': 'v2',
      'src/edited.ts': 'v2',
      'src/new.ts': 'brand new',
    };

    const result = reconcileWrites(projectRoot, proposed, prev);

    expect(Object.keys(result.approvedWrites).sort()).toEqual(['src/new.ts', 'src/pristine.ts']);
    expect(result.skipped.map(s => s.path)).toEqual(['src/edited.ts']);
    // No writes happened
    expect(readFileSync(join(projectRoot, 'src/pristine.ts'), 'utf8')).toBe('v1');
    expect(readFileSync(join(projectRoot, 'src/edited.ts'), 'utf8')).toBe('v1 + user edit');
    expect(result.manifest['src/pristine.ts']).toBe(sha256('v2'));
    expect(result.manifest['src/edited.ts']).toBe(prev['src/edited.ts']);
    expect(result.manifest['src/new.ts']).toBe(sha256('brand new'));
  });
});
