import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { readFileSync, existsSync } from 'node:fs';
import { readFile, rm, mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
import postgres from 'postgres';
import { queueCreate, queueEdit, queueDelete, applyOne, rewindOne, listPending } from '../pending_changes.js';

/**
 * Integration test for the full pending-changes lifecycle.
 * Requires DATABASE_URL env var pointing to a running postgres instance.
 * Skips cleanly when DATABASE_URL is not set.
 *
 * Run with:
 *   DATABASE_URL='postgres://boocode:devpass@localhost:5500/boocode' pnpm -C apps/coder test
 */
describe.runIf(!!process.env.DATABASE_URL)('pending_changes integration', () => {
  let sql: ReturnType<typeof postgres>;
  const testDir = '/tmp/boocode-pending-changes-test-' + Date.now();
  const projectRoot = testDir;
  const testSessionId = '00000000-0000-0000-0000-000000000001';

  beforeAll(async () => {
    sql = postgres(process.env.DATABASE_URL!, { max: 3 });

    // Apply schema
    const schemaPath = resolve(__dirname, '../../schema.sql');
    const ddl = readFileSync(schemaPath, 'utf8');
    await sql.unsafe(ddl);

    // Create temp project directory
    await mkdir(testDir, { recursive: true });
  });

  afterAll(async () => {
    // Cleanup test data
    await sql`DELETE FROM pending_changes WHERE session_id = ${testSessionId}`;
    await sql.end({ timeout: 5 });
    // Remove temp directory
    await rm(testDir, { recursive: true, force: true });
  });

  it('queueCreate → listPending → applyOne → verify file exists', async () => {
    const change = await queueCreate(sql, testSessionId, null, 'hello.txt', 'hello world', projectRoot);
    expect(change.status).toBe('pending');
    expect(change.operation).toBe('create');

    const pending = await listPending(sql, testSessionId);
    expect(pending.some((p) => p.id === change.id)).toBe(true);

    const result = await applyOne(sql, change.id, projectRoot);
    expect(result.success).toBe(true);

    const content = await readFile(resolve(testDir, 'hello.txt'), 'utf8');
    expect(content).toBe('hello world');
  });

  it('queueEdit → apply → verify content changed', async () => {
    // Setup: create a file first
    const createChange = await queueCreate(sql, testSessionId, null, 'editable.txt', 'original content here', projectRoot);
    await applyOne(sql, createChange.id, projectRoot);

    // Queue an edit
    const editChange = await queueEdit(sql, testSessionId, null, 'editable.txt', 'original', 'modified', projectRoot);
    expect(editChange.operation).toBe('edit');

    const result = await applyOne(sql, editChange.id, projectRoot);
    expect(result.success).toBe(true);

    const content = await readFile(resolve(testDir, 'editable.txt'), 'utf8');
    expect(content).toBe('modified content here');
  });

  it('queueDelete → apply → verify file gone', async () => {
    // Setup: create a file
    const createChange = await queueCreate(sql, testSessionId, null, 'deleteme.txt', 'goodbye', projectRoot);
    await applyOne(sql, createChange.id, projectRoot);
    expect(existsSync(resolve(testDir, 'deleteme.txt'))).toBe(true);

    // Queue a delete
    const deleteChange = await queueDelete(sql, testSessionId, null, 'deleteme.txt', projectRoot);
    const result = await applyOne(sql, deleteChange.id, projectRoot);
    expect(result.success).toBe(true);
    expect(existsSync(resolve(testDir, 'deleteme.txt'))).toBe(false);
  });

  it('re-emitted identical edits dedupe at queue and never duplicate on apply', async () => {
    // Regression: the 2-3x block-stamping corruption. An anchored insert queued
    // three times (a local model re-emitting the same tool call) must collapse to
    // ONE pending row and apply exactly once.
    await queueCreate(sql, testSessionId, null, 'dup.js', '<script>\nrender();\n', projectRoot)
      .then((c) => applyOne(sql, c.id, projectRoot));

    const oldStr = '<script>';
    const newStr = '<script>\nconst recordFormats = ["gif"];';
    const a = await queueEdit(sql, testSessionId, null, 'dup.js', oldStr, newStr, projectRoot);
    const b = await queueEdit(sql, testSessionId, null, 'dup.js', oldStr, newStr, projectRoot);
    const c = await queueEdit(sql, testSessionId, null, 'dup.js', oldStr, newStr, projectRoot);
    // All three calls return the SAME pending row (deduped).
    expect(b.id).toBe(a.id);
    expect(c.id).toBe(a.id);

    await applyOne(sql, a.id, projectRoot);
    let content = await readFile(resolve(testDir, 'dup.js'), 'utf8');
    expect((content.match(/const recordFormats/g) || []).length).toBe(1);

    // Even a fresh, separately-queued identical edit re-applied is a no-op, not a stamp.
    const again = await queueEdit(sql, testSessionId, null, 'dup.js', oldStr, newStr, projectRoot);
    const res = await applyOne(sql, again.id, projectRoot);
    expect(res.success).toBe(true);
    content = await readFile(resolve(testDir, 'dup.js'), 'utf8');
    expect((content.match(/const recordFormats/g) || []).length).toBe(1);
  });

  it('preserves CRLF line endings on edit', async () => {
    await queueCreate(sql, testSessionId, null, 'crlf.txt', 'line one\r\nline two\r\nline three\r\n', projectRoot)
      .then((c) => applyOne(sql, c.id, projectRoot));
    const edit = await queueEdit(sql, testSessionId, null, 'crlf.txt', 'line two', 'line TWO', projectRoot);
    const res = await applyOne(sql, edit.id, projectRoot);
    expect(res.success).toBe(true);
    const content = await readFile(resolve(testDir, 'crlf.txt'), 'utf8');
    expect(content).toBe('line one\r\nline TWO\r\nline three\r\n');
  });

  it('refuses an edit that matches multiple locations instead of corrupting', async () => {
    await queueCreate(sql, testSessionId, null, 'ambig.js', 'x=1;\ny=2;\nx=1;\n', projectRoot)
      .then((ch) => applyOne(sql, ch.id, projectRoot));
    const edit = await queueEdit(sql, testSessionId, null, 'ambig.js', 'x=1;', 'x=9;', projectRoot);
    const res = await applyOne(sql, edit.id, projectRoot);
    expect(res.success).toBe(false);
    expect(res.error).toMatch(/matches 2 locations/);
  });

  it('rewindOne → verify reverted', async () => {
    // Setup: create and apply a file
    const createChange = await queueCreate(sql, testSessionId, null, 'rewindable.txt', 'initial', projectRoot);
    await applyOne(sql, createChange.id, projectRoot);

    // Rewind the create (should delete the file)
    const result = await rewindOne(sql, createChange.id, projectRoot);
    expect(result.success).toBe(true);
    expect(existsSync(resolve(testDir, 'rewindable.txt'))).toBe(false);
  });
});
