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

describe('walk — async-iterable depth-first directory traversal', () => {
  let rig: FileTestRig;
  // In-memory tree:
  //   /
  //   ├── a/
  //   │   ├── 1.txt
  //   │   └── b/
  //   │       └── 2.txt
  //   ├── c/
  //   │   └── 3.txt
  //   └── 4.txt
  const tree = new Map<string, FileEntry>();

  function setEntry(path: string, kind: 'file' | 'dir'): void {
    const name = path === '/' ? '' : path.split('/').filter(Boolean).pop() ?? '';
    tree.set(path, { name, kind, size: kind === 'file' ? 10 : 0, mtime: 0, metadata: {} });
  }

  beforeAll(async () => {
    tree.clear();
    setEntry('/', 'dir');
    setEntry('/a', 'dir');
    setEntry('/a/1.txt', 'file');
    setEntry('/a/b', 'dir');
    setEntry('/a/b/2.txt', 'file');
    setEntry('/c', 'dir');
    setEntry('/c/3.txt', 'file');
    setEntry('/4.txt', 'file');

    rig = await setupFileRig({
      list: async (ctx) => {
        if (!tree.has(ctx.path)) throw new NotFoundError(ctx.path);
        const entries: FileEntry[] = [];
        for (const [path, entry] of tree) {
          if (path === ctx.path) continue;
          if (!path.startsWith(ctx.path === '/' ? '/' : ctx.path + '/')) continue;
          const rest = path.slice(ctx.path === '/' ? 1 : ctx.path.length + 1);
          if (rest.includes('/')) continue;
          entries.push(entry);
        }
        return { entries, hasMore: false };
      },
    });
  });

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

  test('walks the entire tree depth-first', async () => {
    const items: { path: string; depth: number }[] = [];
    for await (const item of walk(rig.fs, '/')) {
      items.push({ path: item.relativePath, depth: item.depth });
    }
    // Depth-first order: visit a, then descend into a/* (1.txt + a/b → a/b/2.txt), then c, c/3.txt, then 4.txt
    expect(items.map((i) => i.path)).toEqual([
      'a',
      'a/1.txt',
      'a/b',
      'a/b/2.txt',
      'c',
      'c/3.txt',
      '4.txt',
    ]);
    // Depth values
    expect(items.find((i) => i.path === 'a')?.depth).toBe(1);
    expect(items.find((i) => i.path === 'a/1.txt')?.depth).toBe(2);
    expect(items.find((i) => i.path === 'a/b/2.txt')?.depth).toBe(3);
  });

  test('respects maxDepth', async () => {
    const items: string[] = [];
    for await (const item of walk(rig.fs, '/', { maxDepth: 1 })) {
      items.push(item.relativePath);
    }
    // Only direct children — no descent into /a or /c.
    expect(items.sort()).toEqual(['4.txt', 'a', 'c']);
  });

  test('breaks cleanly when consumer stops iterating', async () => {
    const items: string[] = [];
    for await (const item of walk(rig.fs, '/')) {
      items.push(item.relativePath);
      if (items.length === 2) break;
    }
    expect(items).toEqual(['a', 'a/1.txt']);
  });

  test('filter callback skips entries (and excludes their subtree)', async () => {
    const items: string[] = [];
    for await (const item of walk(rig.fs, '/', {
      filter: (entry, _rel) => entry.name !== 'a',
    })) {
      items.push(item.relativePath);
    }
    expect(items.sort()).toEqual(['4.txt', 'c', 'c/3.txt']);
  });

  test('aborts via signal mid-walk', async () => {
    const ctrl = new AbortController();
    const items: string[] = [];
    setTimeout(() => ctrl.abort(), 5);
    let threw = false;
    try {
      for await (const item of walk(rig.fs, '/', { signal: ctrl.signal })) {
        items.push(item.relativePath);
        await new Promise((r) => setTimeout(r, 10));
      }
    } catch {
      threw = true;
    }
    expect(threw).toBe(true);
  });

  test('walking non-existent path → throws on first list', async () => {
    await expect(async () => {
      for await (const _item of walk(rig.fs, '/nope')) {
        /* unreachable */
      }
    }).toThrow();
  });
});
