// src/engine/reflection/piece-writer.test.ts

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { Repository } from '../../db/repository.js';
import { PieceCatalog } from '../piece-catalog.js';
import { writePiece } from './piece-writer.js';
import type { WritePieceArgs, WritePieceContext } from './piece-writer.js';

// ── Helpers ──────────────────────────────────────────────────────────────────

function makeTmpDir(): string {
  return mkdtempSync(join(tmpdir(), 'piece-writer-test-'));
}

/**
 * Build a minimal test environment:
 *   dir/
 *     db.sqlite       ← Repository
 *     pieces/         ← builtin dir (builtinDir)
 *     data/users/     ← dataDir root
 */
function makeEnv(dir: string) {
  const builtinDir = join(dir, 'pieces');
  const dataDir = join(dir, 'data', 'users');
  mkdirSync(builtinDir, { recursive: true });
  mkdirSync(dataDir, { recursive: true });
  const repo = new Repository(join(dir, 'db.sqlite'));
  const catalog = new PieceCatalog(builtinDir, dataDir);
  const ctx: WritePieceContext = { dataDir, builtinDir };
  return { repo, catalog, ctx, builtinDir, dataDir };
}

function writeBuiltinPiece(builtinDir: string, name: string, yaml = 'movements:\n  - name: execute\n') {
  writeFileSync(join(builtinDir, `${name}.yaml`), yaml);
}

function userPieceDir(dataDir: string, userId: string): string {
  return join(dataDir, userId, 'pieces');
}

function userPiecePath(dataDir: string, userId: string, name: string): string {
  return join(userPieceDir(dataDir, userId), `${name}.yaml`);
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('writePiece — happy path (builtin source)', () => {
  let dir: string;

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

  it('forks the builtin and writes the new YAML, records edit, invalidates catalog', async () => {
    const { repo, catalog, ctx, builtinDir, dataDir } = makeEnv(dir);
    const userId = 'user1';
    const pieceName = 'chat';
    const newYaml = 'movements:\n  - name: improved\n';

    writeBuiltinPiece(builtinDir, pieceName);

    // Pre-assert: user piece does not exist yet.
    expect(existsSync(userPiecePath(dataDir, userId, pieceName))).toBe(false);

    // Warm the catalog cache so we can verify invalidation.
    catalog.getForUser(userId);

    const args: WritePieceArgs = {
      userId,
      pieceName,
      pieceSource: 'builtin',
      newYaml,
      snapshotId: 'snap-001',
      cooldownHours: 24,
    };

    const result = await writePiece(args, ctx, repo, catalog);
    expect(result.written).toBe(true);

    // Destination file must contain the new YAML.
    const written = readFileSync(userPiecePath(dataDir, userId, pieceName), 'utf-8');
    expect(written).toBe(newYaml);

    // DB row must exist.
    const count = repo.countRecentPieceEdits(userId, pieceName, 24 * 3600 * 1000);
    expect(count).toBe(1);

    // Catalog cache must have been invalidated: calling getForUser should
    // re-read disk and pick up the new custom piece.
    const entries = catalog.getForUser(userId);
    const entry = entries.find(e => e.name === pieceName);
    expect(entry).toBeTruthy();
    expect(entry?.source).toBe('custom');
  });
});

describe('writePiece — cooldown gate', () => {
  let dir: string;

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

  it('returns { written: false, reason: "cooldown" } after 2 edits within the window', async () => {
    const { repo, catalog, ctx, builtinDir, dataDir } = makeEnv(dir);
    const userId = 'user2';
    const pieceName = 'research';

    writeBuiltinPiece(builtinDir, pieceName);

    // Pre-insert 2 edits within the cooldown window directly with distinct
    // timestamps (avoid PK collision if both happen in the same millisecond).
    const now = Date.now();
    repo.getDb()
      .prepare(
        `INSERT INTO reflection_piece_edits (user_id, piece_name, snapshot_id, created_at)
         VALUES (?, ?, ?, ?)`
      )
      .run(userId, pieceName, 'snap-pre-1', now - 2);
    repo.getDb()
      .prepare(
        `INSERT INTO reflection_piece_edits (user_id, piece_name, snapshot_id, created_at)
         VALUES (?, ?, ?, ?)`
      )
      .run(userId, pieceName, 'snap-pre-2', now - 1);

    const args: WritePieceArgs = {
      userId,
      pieceName,
      pieceSource: 'builtin',
      newYaml: 'movements:\n  - name: blocked\n',
      snapshotId: 'snap-003',
      cooldownHours: 24,
    };

    const result = await writePiece(args, ctx, repo, catalog);
    expect(result.written).toBe(false);
    if (!result.written) {
      expect(result.reason).toBe('cooldown');
    }

    // No fork should have happened.
    expect(existsSync(userPiecePath(dataDir, userId, pieceName))).toBe(false);

    // Edit count must remain at 2 (the third was not recorded).
    const count = repo.countRecentPieceEdits(userId, pieceName, 24 * 3600 * 1000);
    expect(count).toBe(2);
  });
});

describe('writePiece — custom source (already exists)', () => {
  let dir: string;

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

  it('overwrites the existing custom piece without calling silentFork', async () => {
    const { repo, catalog, ctx, builtinDir, dataDir } = makeEnv(dir);
    const userId = 'user3';
    const pieceName = 'general';
    const newYaml = 'movements:\n  - name: updated\n';

    // Pre-create a custom piece (simulating a previously forked piece).
    const pDir = userPieceDir(dataDir, userId);
    mkdirSync(pDir, { recursive: true });
    writeFileSync(userPiecePath(dataDir, userId, pieceName), 'movements:\n  - name: old\n');

    // No builtin needed when pieceSource === 'custom'.
    const args: WritePieceArgs = {
      userId,
      pieceName,
      pieceSource: 'custom',
      newYaml,
      snapshotId: 'snap-custom-1',
      cooldownHours: 24,
    };

    const result = await writePiece(args, ctx, repo, catalog);
    expect(result.written).toBe(true);

    const written = readFileSync(userPiecePath(dataDir, userId, pieceName), 'utf-8');
    expect(written).toBe(newYaml);

    const count = repo.countRecentPieceEdits(userId, pieceName, 24 * 3600 * 1000);
    expect(count).toBe(1);
  });
});

describe('writePiece — atomic rename', () => {
  let dir: string;

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

  it('destination file contents match newYaml exactly after a successful write', async () => {
    const { repo, catalog, ctx, builtinDir, dataDir } = makeEnv(dir);
    const userId = 'user4';
    const pieceName = 'slide';
    const newYaml = '---\nforked_from_commit: unknown\n---\nmovements:\n  - name: atomic\n';

    writeBuiltinPiece(builtinDir, pieceName);

    const args: WritePieceArgs = {
      userId,
      pieceName,
      pieceSource: 'builtin',
      newYaml,
      snapshotId: 'snap-atomic-1',
      cooldownHours: 24,
    };

    const result = await writePiece(args, ctx, repo, catalog);
    expect(result.written).toBe(true);

    const dst = userPiecePath(dataDir, userId, pieceName);
    expect(existsSync(dst)).toBe(true);

    // Exact content match — no partial write.
    expect(readFileSync(dst, 'utf-8')).toBe(newYaml);

    // No .tmp file left behind.
    const tmpFiles = readdirSync(userPieceDir(dataDir, userId))
      .filter(f => f.includes('.tmp.'));
    expect(tmpFiles).toHaveLength(0);
  });
});
