/**
 * Tests for file tree builder (lazy loading version).
 */

import { mkdir, mkdir as mkdirAsync, rmdir, symlink, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { readTree } from "../../fs/tree.js";

describe("readTree", () => {
  let testDir: string;

  beforeEach(async () => {
    testDir = join(tmpdir(), `tree-test-${Date.now()}`);
    await mkdir(testDir);
  });

  afterEach(async () => {
    try {
      await rmdir(testDir, { recursive: true });
    } catch {
      // Ignore cleanup errors
    }
  });

  it("should return empty children array for empty directory", async () => {
    const result = await readTree(testDir);
    expect(result.path).toBe(".");
    expect(result.children).toEqual([]);
  });

  it("should list files and directories at root level", async () => {
    await writeFile(join(testDir, "file.txt"), "content");
    await mkdirAsync(join(testDir, "subdir"));
    await writeFile(join(testDir, "subdir", "nested.txt"), "nested");

    const result = await readTree(testDir);

    expect(result.children).toHaveLength(2);

    const dir = result.children.find((n) => n.name === "subdir");
    expect(dir).toBeDefined();
    expect(dir?.kind).toBe("dir");
    expect(dir?.children).toBeUndefined(); // Lazy loading: children undefined

    const file = result.children.find((n) => n.name === "file.txt");
    expect(file).toBeDefined();
    expect(file?.kind).toBe("file");
    expect(file?.size).toBe(7);
    expect(file?.mtime).toBeDefined();
  });

  it("should return children of subdir when subPath specified", async () => {
    await mkdirAsync(join(testDir, "subdir"));
    await writeFile(join(testDir, "subdir", "nested.txt"), "nested");
    await writeFile(join(testDir, "subdir", "another.txt"), "another");

    const result = await readTree(testDir, "subdir");

    expect(result.path).toBe("subdir");
    expect(result.children).toHaveLength(2);
    // Paths are relative to root, include subdir prefix
    expect(result.children[0].path).toBe("subdir/another.txt");
    expect(result.children[1].path).toBe("subdir/nested.txt");
  });

  it("should sort directories before files", async () => {
    await writeFile(join(testDir, "z-file.txt"), "content");
    await mkdirAsync(join(testDir, "a-dir"));

    const result = await readTree(testDir);

    expect(result.children[0].name).toBe("a-dir");
    expect(result.children[0].kind).toBe("dir");
    expect(result.children[1].name).toBe("z-file.txt");
    expect(result.children[1].kind).toBe("file");
  });

  it("should sort items alphabetically within same kind", async () => {
    await mkdirAsync(join(testDir, "b-dir"));
    await mkdirAsync(join(testDir, "a-dir"));
    await writeFile(join(testDir, "b-file.txt"), "b");
    await writeFile(join(testDir, "a-file.txt"), "a");

    const result = await readTree(testDir);

    expect(result.children[0].name).toBe("a-dir");
    expect(result.children[1].name).toBe("b-dir");
    expect(result.children[2].name).toBe("a-file.txt");
    expect(result.children[3].name).toBe("b-file.txt");
  });

  it("should show hidden files except .git", async () => {
    await writeFile(join(testDir, ".hidden"), "hidden");
    await writeFile(join(testDir, ".gitignore"), "*.log\n");
    await mkdirAsync(join(testDir, ".git"));
    await writeFile(join(testDir, "visible.txt"), "visible");

    const result = await readTree(testDir);

    expect(result.children.some((n) => n.name === ".hidden")).toBe(true);
    expect(result.children.some((n) => n.name === ".gitignore")).toBe(true);
    expect(result.children.some((n) => n.name === ".git")).toBe(false);
    expect(result.children.some((n) => n.name === "visible.txt")).toBe(true);
  });

  it("should show node_modules but skip .git", async () => {
    await mkdirAsync(join(testDir, "node_modules"));
    await mkdirAsync(join(testDir, ".git"));
    await writeFile(join(testDir, "file.txt"), "content");

    const result = await readTree(testDir);

    expect(result.children.some((n) => n.name === "node_modules")).toBe(true);
    expect(result.children.some((n) => n.name === ".git")).toBe(false);
    expect(result.children.some((n) => n.name === "file.txt")).toBe(true);
  });

  it("should use relative paths", async () => {
    await mkdirAsync(join(testDir, "subdir"));
    await writeFile(join(testDir, "subdir", "file.txt"), "content");

    const result = await readTree(testDir);

    expect(result.children[0].path).toBe("subdir");
    // Subdir children are undefined (lazy loading)
  });

  it("should keep ignored entries visible while marking gitignored metadata", async () => {
    await writeFile(join(testDir, ".gitignore"), "*.log\ndist/");
    await writeFile(join(testDir, "app.log"), "log content");
    await writeFile(join(testDir, "app.txt"), "text content");
    await mkdirAsync(join(testDir, "dist"));
    await mkdirAsync(join(testDir, "src"));

    const result = await readTree(testDir);

    const ignoredLog = result.children.find((n) => n.name === "app.log");
    const ignoredDist = result.children.find((n) => n.name === "dist");
    const visibleFile = result.children.find((n) => n.name === "app.txt");
    const visibleDir = result.children.find((n) => n.name === "src");

    expect(ignoredLog).toMatchObject({
      name: "app.log",
      kind: "file",
      isGitIgnored: true,
    });
    expect(ignoredDist).toMatchObject({
      name: "dist",
      kind: "dir",
      isGitIgnored: true,
    });
    expect(visibleFile).toMatchObject({
      name: "app.txt",
      kind: "file",
      isGitIgnored: false,
    });
    expect(visibleDir).toMatchObject({
      name: "src",
      kind: "dir",
      isGitIgnored: false,
    });
  });

  it("shows symlinked files and directories using the target kind", async () => {
    await mkdirAsync(join(testDir, "real-dir"));
    await writeFile(join(testDir, "real-file.txt"), "content");
    await symlink(join(testDir, "real-dir"), join(testDir, "linked-dir"), "dir");
    await symlink(join(testDir, "real-file.txt"), join(testDir, "linked-file.txt"));

    const result = await readTree(testDir);

    expect(result.children).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          name: "linked-dir",
          path: "linked-dir",
          kind: "dir",
          isSymlink: true,
        }),
        expect.objectContaining({
          name: "linked-file.txt",
          path: "linked-file.txt",
          kind: "file",
          isSymlink: true,
        }),
      ])
    );
  });

  it("lists external symlink directories but rejects expanding them", async () => {
    const outsideDir = join(tmpdir(), `tree-test-outside-${Date.now()}`);
    await mkdirAsync(outsideDir);
    await symlink(outsideDir, join(testDir, "external-dir"), "dir");

    const rootResult = await readTree(testDir);
    expect(rootResult.children).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          name: "external-dir",
          kind: "dir",
          isSymlink: true,
        }),
      ])
    );

    await expect(readTree(testDir, "external-dir")).rejects.toMatchObject({
      code: "path_escape",
    });

    await rmdir(outsideDir);
  });
});
