import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, utimesSync, existsSync, statSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { runTrashCleanup, startTrashCleanup } from './trash-cleanup.js';

const DAY_MS = 86_400_000;

function ageFile(path: string, daysOld: number): void {
  const t = (Date.now() - daysOld * DAY_MS) / 1000;
  utimesSync(path, t, t);
}

function setupUser(root: string, userId: string): string {
  const trash = join(root, userId, 'trash');
  mkdirSync(trash, { recursive: true });
  return trash;
}

describe('user-folder/trash-cleanup', () => {
  let root: string;

  beforeEach(() => {
    root = mkdtempSync(join(tmpdir(), 'trash-cleanup-test-'));
  });

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

  it('deletes files older than retentionDays and keeps fresh ones', async () => {
    const trash = setupUser(root, 'alice');
    const stale = join(trash, 'stale.txt');
    const fresh = join(trash, 'fresh.txt');
    writeFileSync(stale, 'old');
    writeFileSync(fresh, 'new');
    ageFile(stale, 40);
    ageFile(fresh, 5);

    const result = await runTrashCleanup(root, 30);

    expect(result.scannedUsers).toBe(1);
    expect(result.deletedFiles).toBe(1);
    expect(existsSync(stale)).toBe(false);
    expect(existsSync(fresh)).toBe(true);
  });

  it('returns 0 deletes when nothing is eligible', async () => {
    const trash = setupUser(root, 'alice');
    const fresh = join(trash, 'fresh.txt');
    writeFileSync(fresh, 'new');
    ageFile(fresh, 5);

    const result = await runTrashCleanup(root, 30);

    expect(result.scannedUsers).toBe(1);
    expect(result.deletedFiles).toBe(0);
    expect(existsSync(fresh)).toBe(true);
  });

  it('sweeps every user under the root', async () => {
    const aliceTrash = setupUser(root, 'alice');
    const bobTrash = setupUser(root, 'bob');
    const charlieTrash = setupUser(root, 'charlie');
    const aliceStale = join(aliceTrash, 'a.txt');
    const bobStale = join(bobTrash, 'b.txt');
    const charlieFresh = join(charlieTrash, 'c.txt');
    writeFileSync(aliceStale, 'a');
    writeFileSync(bobStale, 'b');
    writeFileSync(charlieFresh, 'c');
    ageFile(aliceStale, 100);
    ageFile(bobStale, 100);
    ageFile(charlieFresh, 1);

    const result = await runTrashCleanup(root, 30);

    expect(result.scannedUsers).toBe(3);
    expect(result.deletedFiles).toBe(2);
    expect(existsSync(aliceStale)).toBe(false);
    expect(existsSync(bobStale)).toBe(false);
    expect(existsSync(charlieFresh)).toBe(true);
  });

  it('retentionDays=0 deletes every file regardless of age', async () => {
    const trash = setupUser(root, 'alice');
    const fresh = join(trash, 'fresh.txt');
    writeFileSync(fresh, 'just now');
    // no ageFile — mtime is essentially Date.now()

    const result = await runTrashCleanup(root, 0);

    expect(result.deletedFiles).toBe(1);
    expect(existsSync(fresh)).toBe(false);
  });

  it('recurses into nested directories and removes empties', async () => {
    const trash = setupUser(root, 'alice');
    const nestedDir = join(trash, 'project/old');
    mkdirSync(nestedDir, { recursive: true });
    const nested = join(nestedDir, 'leaf.txt');
    writeFileSync(nested, 'deep');
    ageFile(nested, 100);

    const result = await runTrashCleanup(root, 30);

    expect(result.deletedFiles).toBe(1);
    expect(existsSync(nested)).toBe(false);
    expect(existsSync(nestedDir)).toBe(false);
    expect(existsSync(join(trash, 'project'))).toBe(false);
  });

  it('skips users that have no trash/ dir', async () => {
    mkdirSync(join(root, 'noTrash', 'scripts'), { recursive: true });
    const aliceTrash = setupUser(root, 'alice');
    const stale = join(aliceTrash, 'a.txt');
    writeFileSync(stale, 'a');
    ageFile(stale, 100);

    const result = await runTrashCleanup(root, 30);

    expect(result.scannedUsers).toBe(1); // only alice
    expect(result.deletedFiles).toBe(1);
  });

  it('returns empty result when root does not exist', async () => {
    const missing = join(root, 'does-not-exist');
    const result = await runTrashCleanup(missing, 30);
    expect(result).toEqual({ scannedUsers: 0, deletedFiles: 0, freedBytes: 0 });
  });

  it('rejects negative retentionDays', async () => {
    await expect(runTrashCleanup(root, -1)).rejects.toThrow(/non-negative/);
  });

  it('startTrashCleanup runs an immediate sweep and the interval is unref-safe', async () => {
    const trash = setupUser(root, 'alice');
    const stale = join(trash, 'a.txt');
    writeFileSync(stale, 'a');
    ageFile(stale, 100);

    const handle = startTrashCleanup({
      userFolderRoot: root,
      retentionDays: 30,
      intervalMs: 60_000,
    });
    try {
      // Await the boot sweep directly instead of polling — this was flaky
      // when concurrent vitest suites starved the setImmediate queue.
      await handle.initialSweep;
      expect(existsSync(stale)).toBe(false);
    } finally {
      handle.stop();
    }
  });

  it('tracks freedBytes from deleted files', async () => {
    const trash = setupUser(root, 'alice');
    const stale = join(trash, 'big.txt');
    writeFileSync(stale, 'x'.repeat(1024));
    ageFile(stale, 100);

    const result = await runTrashCleanup(root, 30);

    expect(result.deletedFiles).toBe(1);
    expect(result.freedBytes).toBe(1024);
  });

  it('keeps fresh files when sibling stale files are removed', async () => {
    const trash = setupUser(root, 'alice');
    const stale = join(trash, 'old.txt');
    const fresh = join(trash, 'new.txt');
    writeFileSync(stale, 'x');
    writeFileSync(fresh, 'y');
    ageFile(stale, 60);
    ageFile(fresh, 1);

    await runTrashCleanup(root, 30);

    expect(existsSync(stale)).toBe(false);
    expect(existsSync(fresh)).toBe(true);
    expect(statSync(fresh).size).toBe(1);
  });
});
