// src/engine/reflection/retention.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
  mkdtempSync,
  rmSync,
  mkdirSync,
  writeFileSync,
  existsSync,
} from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
  pruneOldSnapshots,
  enforceDiskCap,
  runReflectionRetentionSweep,
  type RetentionDeps,
} from './retention.js';

// ── Fixtures ──────────────────────────────────────────────────────────────────

const USER_ID = 'u-retention-test';

/** Format a Date as the snapshot directory name prefix (YYYYMMDDTHHmmssZ). */
function fmtTs(d: Date): string {
  const pad2 = (n: number) => String(n).padStart(2, '0');
  return (
    `${d.getUTCFullYear()}` +
    `${pad2(d.getUTCMonth() + 1)}` +
    `${pad2(d.getUTCDate())}` +
    `T${pad2(d.getUTCHours())}` +
    `${pad2(d.getUTCMinutes())}` +
    `${pad2(d.getUTCSeconds())}` +
    `Z`
  );
}

/** Create a fake snapshot dir for the given age in days (relative to now). */
function makeSnapshot(
  histDir: string,
  ageDays: number,
  jobId: string,
  fileSizeBytes = 100,
): string {
  const d = new Date(Date.now() - ageDays * 86_400_000);
  const name = `${fmtTs(d)}-${jobId}`;
  const dir = join(histDir, name);
  mkdirSync(dir, { recursive: true });
  // Write a dummy meta.json + a content file of the requested size
  writeFileSync(
    join(dir, 'meta.json'),
    JSON.stringify({ snapshotId: name, ts: d.toISOString(), pieceName: 'chat' }),
    'utf-8',
  );
  writeFileSync(join(dir, 'data.bin'), Buffer.alloc(fileSizeBytes), 'binary');
  return name;
}

function makeDeps(dataDir: string): RetentionDeps {
  return { dataDir };
}

function histDir(dataDir: string, userId: string): string {
  return join(dataDir, userId, '.reflection-history');
}

// ── Test suite ────────────────────────────────────────────────────────────────

describe('pruneOldSnapshots', () => {
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'retention-age-'));
  });

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

  it('does NOT prune a snapshot newer than retentionDays', async () => {
    const hDir = histDir(tmpDir, USER_ID);
    mkdirSync(hDir, { recursive: true });
    const snapId = makeSnapshot(hDir, 5, 'j-new'); // 5 days old

    const result = await pruneOldSnapshots(makeDeps(tmpDir), USER_ID, 90);

    expect(result.pruned).toHaveLength(0);
    expect(existsSync(join(hDir, snapId))).toBe(true);
  });

  it('prunes a snapshot older than retentionDays', async () => {
    const hDir = histDir(tmpDir, USER_ID);
    mkdirSync(hDir, { recursive: true });
    const snapId = makeSnapshot(hDir, 100, 'j-old'); // 100 days old, retention=90

    const result = await pruneOldSnapshots(makeDeps(tmpDir), USER_ID, 90);

    expect(result.pruned).toContain(snapId);
    expect(existsSync(join(hDir, snapId))).toBe(false);
  });

  it('prunes old but keeps new when both present', async () => {
    const hDir = histDir(tmpDir, USER_ID);
    mkdirSync(hDir, { recursive: true });
    const oldId = makeSnapshot(hDir, 200, 'j-old');  // old
    const newId = makeSnapshot(hDir, 10, 'j-new');   // new

    const result = await pruneOldSnapshots(makeDeps(tmpDir), USER_ID, 90);

    expect(result.pruned).toContain(oldId);
    expect(result.pruned).not.toContain(newId);
    expect(existsSync(join(hDir, oldId))).toBe(false);
    expect(existsSync(join(hDir, newId))).toBe(true);
  });

  it('returns empty pruned when no history dir exists', async () => {
    const result = await pruneOldSnapshots(makeDeps(tmpDir), 'u-ghost', 90);
    expect(result.pruned).toHaveLength(0);
  });

  it('does not prune index.jsonl (not a parseable snapshot dir name)', async () => {
    const hDir = histDir(tmpDir, USER_ID);
    mkdirSync(hDir, { recursive: true });
    // Write index.jsonl as a file (not a dir), should be ignored
    writeFileSync(join(hDir, 'index.jsonl'), '', 'utf-8');
    const snapId = makeSnapshot(hDir, 5, 'j-ok');

    const result = await pruneOldSnapshots(makeDeps(tmpDir), USER_ID, 90);

    expect(result.pruned).toHaveLength(0);
    expect(existsSync(join(hDir, 'index.jsonl'))).toBe(true);
    expect(existsSync(join(hDir, snapId))).toBe(true);
  });
});

describe('enforceDiskCap', () => {
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'retention-cap-'));
  });

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

  it('does not prune when total bytes is under cap', async () => {
    const hDir = histDir(tmpDir, USER_ID);
    mkdirSync(hDir, { recursive: true });
    // Each snapshot ≈ 200 bytes (100 data + small meta.json); cap = 10 MiB
    const snapId = makeSnapshot(hDir, 5, 'j-small', 100);

    const cap = 10 * 1024 * 1024; // 10 MiB
    const result = await enforceDiskCap(makeDeps(tmpDir), USER_ID, cap);

    expect(result.pruned).toHaveLength(0);
    expect(existsSync(join(hDir, snapId))).toBe(true);
  });

  it('prunes oldest snapshots first when over cap', async () => {
    const hDir = histDir(tmpDir, USER_ID);
    mkdirSync(hDir, { recursive: true });

    // Create 3 snapshots: oldest, middle, newest — each ~1024 bytes of data
    const oldestId = makeSnapshot(hDir, 30, 'j-oldest', 1024);
    const middleId = makeSnapshot(hDir, 20, 'j-middle', 1024);
    const newestId = makeSnapshot(hDir, 10, 'j-newest', 1024);

    // The total will be around 3 * (1024 + meta) bytes.
    // Set cap so that only 2 snapshots fit (i.e., total > 2 * entry_size).
    // Each entry is ~1024 + ~80 bytes meta ≈ 1104 bytes.
    // Cap at 2200 bytes → oldest must be pruned.
    const cap = 2200;

    const result = await enforceDiskCap(makeDeps(tmpDir), USER_ID, cap);

    // oldest should be pruned, newest should survive
    expect(result.pruned).toContain(oldestId);
    expect(existsSync(join(hDir, oldestId))).toBe(false);
    expect(existsSync(join(hDir, newestId))).toBe(true);
    // middle may or may not be pruned depending on actual sizes, but newest must survive
    void middleId; // referenced to suppress unused variable warning
  });

  it('newest snapshot survives when only one exists and under cap', async () => {
    const hDir = histDir(tmpDir, USER_ID);
    mkdirSync(hDir, { recursive: true });
    const snapId = makeSnapshot(hDir, 5, 'j-only', 100);

    const cap = 10 * 1024 * 1024; // 10 MiB — easily above 100 bytes
    const result = await enforceDiskCap(makeDeps(tmpDir), USER_ID, cap);

    expect(result.pruned).toHaveLength(0);
    expect(existsSync(join(hDir, snapId))).toBe(true);
  });

  it('returns empty pruned when no history dir exists', async () => {
    const result = await enforceDiskCap(makeDeps(tmpDir), 'u-ghost', 1024);
    expect(result.pruned).toHaveLength(0);
  });
});

describe('runReflectionRetentionSweep', () => {
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'retention-sweep-'));
  });

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

  it('sweeps all users and prunes old snapshots', async () => {
    const uid1 = 'u-sweep-1';
    const uid2 = 'u-sweep-2';

    const hDir1 = histDir(tmpDir, uid1);
    const hDir2 = histDir(tmpDir, uid2);
    mkdirSync(hDir1, { recursive: true });
    mkdirSync(hDir2, { recursive: true });

    const old1 = makeSnapshot(hDir1, 100, 'j-old1'); // old
    const new1 = makeSnapshot(hDir1, 5, 'j-new1');   // new
    const old2 = makeSnapshot(hDir2, 200, 'j-old2'); // old

    await runReflectionRetentionSweep(makeDeps(tmpDir), {
      snapshotRetentionDays: 90,
      snapshotMaxBytesPerUser: 100 * 1024 * 1024, // 100 MiB — won't trigger
    });

    expect(existsSync(join(hDir1, old1))).toBe(false);
    expect(existsSync(join(hDir1, new1))).toBe(true);
    expect(existsSync(join(hDir2, old2))).toBe(false);
  });

  it('skips users with no .reflection-history dir', async () => {
    // Create a user dir without a history dir
    const uid = 'u-no-history';
    mkdirSync(join(tmpDir, uid), { recursive: true });

    // Should not throw
    await expect(
      runReflectionRetentionSweep(makeDeps(tmpDir), {
        snapshotRetentionDays: 90,
        snapshotMaxBytesPerUser: 100 * 1024 * 1024,
      }),
    ).resolves.toBeUndefined();
  });

  it('handles a non-existent dataDir gracefully', async () => {
    await expect(
      runReflectionRetentionSweep(
        { dataDir: join(tmpDir, 'does-not-exist') },
        { snapshotRetentionDays: 90, snapshotMaxBytesPerUser: 100 * 1024 * 1024 },
      ),
    ).resolves.toBeUndefined();
  });

  it('single snapshot under cap and within retention is not touched', async () => {
    const uid = 'u-clean';
    const hDir = histDir(tmpDir, uid);
    mkdirSync(hDir, { recursive: true });
    const snapId = makeSnapshot(hDir, 5, 'j-only', 100);

    await runReflectionRetentionSweep(makeDeps(tmpDir), {
      snapshotRetentionDays: 90,
      snapshotMaxBytesPerUser: 100 * 1024 * 1024,
    });

    expect(existsSync(join(hDir, snapId))).toBe(true);
  });
});

