import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import {
  ConflictError,
  IdempotencyConflictError,
  NotFoundError,
  PermissionDeniedError,
  type FileEntry,
} from '../../src/index.js';
import { setupFileRig, type FileTestRig } from './helpers/rig.js';

describe('Standard ops — list/stat/mkdir/delete/move E2E', () => {
  let rig: FileTestRig;
  // Simple in-memory backing store on Bob.
  const tree = new Map<string, FileEntry>();

  beforeAll(async () => {
    tree.clear();
    tree.set('/', { name: '', kind: 'dir', size: 0, mtime: 0, metadata: {} });
    tree.set('/foo', { name: 'foo', kind: 'dir', size: 0, mtime: 100, metadata: {} });
    tree.set('/foo/bar.txt', {
      name: 'bar.txt',
      kind: 'file',
      size: 12,
      mtime: 200,
      contentType: 'text/plain',
      metadata: {},
    });
    tree.set('/foo/baz.txt', {
      name: 'baz.txt',
      kind: 'file',
      size: 5,
      mtime: 300,
      metadata: {},
    });

    rig = await setupFileRig({
      list: async (ctx) => {
        const dir = ctx.path;
        const entries: FileEntry[] = [];
        for (const [path, entry] of tree) {
          if (path === dir) continue;
          if (!path.startsWith(dir === '/' ? '/' : dir + '/')) continue;
          const rest = path.slice(dir === '/' ? 1 : dir.length + 1);
          if (rest.includes('/')) continue;
          entries.push(entry);
        }
        return { entries, hasMore: false };
      },
      stat: async (ctx) => {
        const e = tree.get(ctx.path);
        if (e === undefined) throw new NotFoundError(`${ctx.path} not found`);
        return e;
      },
      mkdir: async (ctx) => {
        if (tree.has(ctx.path)) throw new ConflictError(`${ctx.path} exists`);
        const name = ctx.path.split('/').filter(Boolean).pop() ?? '';
        const entry: FileEntry = {
          name,
          kind: 'dir',
          size: 0,
          mtime: Date.now(),
          metadata: {},
        };
        tree.set(ctx.path, entry);
        return { entry };
      },
      delete: async (ctx) => {
        if (!tree.has(ctx.path)) throw new NotFoundError(ctx.path);
        let count = 0;
        if (ctx.args.recursive) {
          for (const path of [...tree.keys()]) {
            if (path === ctx.path || path.startsWith(ctx.path + '/')) {
              tree.delete(path);
              count++;
            }
          }
        } else {
          tree.delete(ctx.path);
          count = 1;
        }
        return { deletedCount: count };
      },
      move: async (ctx) => {
        const src = ctx.args.src;
        const dst = ctx.args.dst;
        const e = tree.get(src);
        if (e === undefined) throw new NotFoundError(src);
        if (tree.has(dst) && !ctx.args.overwrite) {
          throw new ConflictError(`${dst} exists`);
        }
        const newName = dst.split('/').filter(Boolean).pop() ?? e.name;
        tree.delete(src);
        tree.set(dst, { ...e, name: newName });
        return { entry: tree.get(dst)! };
      },
    });
  });

  afterAll(async () => {
    await rig.teardown();
  });

  test('list /', async () => {
    const page = await rig.fs.list('/');
    expect(page.hasMore).toBe(false);
    const names = page.entries.map((e) => e.name).sort();
    expect(names).toEqual(['foo']);
  });

  test('list /foo', async () => {
    const page = await rig.fs.list('/foo');
    const names = page.entries.map((e) => e.name).sort();
    expect(names).toEqual(['bar.txt', 'baz.txt']);
  });

  test('stat existing file', async () => {
    const e = await rig.fs.stat('/foo/bar.txt');
    expect(e.size).toBe(12);
    expect(e.contentType).toBe('text/plain');
  });

  test('stat missing → NotFoundError', async () => {
    let caught: unknown = null;
    try {
      await rig.fs.stat('/no/such/file');
    } catch (err) {
      caught = err;
    }
    expect(caught instanceof NotFoundError).toBe(true);
    expect((caught as NotFoundError).payload.code).toBe('NOT_FOUND');
  });

  test('mkdir creates a new directory', async () => {
    const result = await rig.fs.mkdir('/created');
    expect(result.entry.kind).toBe('dir');
    expect(tree.has('/created')).toBe(true);
  });

  test('mkdir on existing path → ConflictError', async () => {
    await expect(rig.fs.mkdir('/foo')).rejects.toBeInstanceOf(ConflictError);
  });

  test('delete file', async () => {
    tree.set('/temp.txt', { name: 'temp.txt', kind: 'file', size: 0, mtime: 0, metadata: {} });
    const r = await rig.fs.delete('/temp.txt');
    expect(r.deletedCount).toBe(1);
    expect(tree.has('/temp.txt')).toBe(false);
  });

  test('delete missing → NotFoundError', async () => {
    await expect(rig.fs.delete('/doesnt-exist')).rejects.toBeInstanceOf(NotFoundError);
  });

  test('move file', async () => {
    tree.set('/x.txt', { name: 'x.txt', kind: 'file', size: 0, mtime: 0, metadata: {} });
    const r = await rig.fs.move('/x.txt', '/y.txt');
    expect(r.entry.name).toBe('y.txt');
    expect(tree.has('/x.txt')).toBe(false);
    expect(tree.has('/y.txt')).toBe(true);
  });

  test('move overwrite=false → ConflictError on collision', async () => {
    tree.set('/a.txt', { name: 'a.txt', kind: 'file', size: 0, mtime: 0, metadata: {} });
    tree.set('/b.txt', { name: 'b.txt', kind: 'file', size: 0, mtime: 0, metadata: {} });
    await expect(rig.fs.move('/a.txt', '/b.txt')).rejects.toBeInstanceOf(ConflictError);
  });

  test('idempotent retry: same key + same args → cached response', async () => {
    tree.set('/idem.txt', { name: 'idem.txt', kind: 'file', size: 0, mtime: 0, metadata: {} });
    const key = 'IdemKey1AaBbCcDdEeFfGg'; // 22 chars
    const r1 = await rig.fs.delete('/idem.txt', { idempotencyKey: key });
    expect(r1.deletedCount).toBe(1);
    // 2nd call with same key → same result without throwing NotFound
    const r2 = await rig.fs.delete('/idem.txt', { idempotencyKey: key });
    expect(r2.deletedCount).toBe(1);
  });

  test('idempotency conflict: same key + different args', async () => {
    tree.set('/c1.txt', { name: 'c1.txt', kind: 'file', size: 0, mtime: 0, metadata: {} });
    tree.set('/c2.txt', { name: 'c2.txt', kind: 'file', size: 0, mtime: 0, metadata: {} });
    const key = 'ConflictKeyAaBbCcDdEeF'; // 22 chars
    await rig.fs.delete('/c1.txt', { idempotencyKey: key });
    await expect(
      rig.fs.delete('/c2.txt', { idempotencyKey: key }),
    ).rejects.toBeInstanceOf(IdempotencyConflictError);
  });

  test('path validation: traversal rejected by server', async () => {
    await expect(rig.fs.list('/foo')).resolves.toBeDefined();
    // Client-side schema rejects traversal too — bypass via direct bad path:
    await expect(rig.fs.stat('/foo/../etc')).rejects.toThrow();
  });

  // We don't test PermissionDenied here (no beforeOp gate configured),
  // but the type is referenced to ensure it's exported properly.
  test('PermissionDeniedError is exported', () => {
    expect(PermissionDeniedError).toBeDefined();
  });
});
