import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { mkdtemp, mkdir, rm, realpath, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { resolveProjectPath } from '../projects.js';

describe('resolveProjectPath', () => {
  let scratch: string;
  let whitelist: string;
  let outside: string;

  beforeAll(async () => {
    // mkdtemp returns a real path on most platforms, but symlink-resolve it
    // anyway to defeat /tmp -> /private/tmp style indirection on macOS, and
    // keep this stable for the comparisons below.
    scratch = await realpath(await mkdtemp(join(tmpdir(), 'boocode-projects-test-')));
    whitelist = join(scratch, 'wl');
    outside = join(scratch, 'other');
    await mkdir(whitelist, { recursive: true });
    await mkdir(outside, { recursive: true });
  });

  afterAll(async () => {
    if (scratch) {
      await rm(scratch, { recursive: true, force: true });
    }
  });

  it('returns real path and basename for a valid subdirectory under the whitelist', async () => {
    const projectDir = join(whitelist, 'my-project');
    await mkdir(projectDir);
    const result = await resolveProjectPath(projectDir, whitelist);
    expect('error' in result).toBe(false);
    if ('error' in result) return; // narrow
    expect(result.real).toBe(projectDir);
    expect(result.name).toBe('my-project');
  });

  it('rejects a path outside the whitelist', async () => {
    const projectDir = join(outside, 'foo');
    await mkdir(projectDir);
    const result = await resolveProjectPath(projectDir, whitelist);
    expect('error' in result).toBe(true);
    if (!('error' in result)) return;
    expect(result.error.toLowerCase()).toContain('path must be under');
  });

  it('rejects relative paths', async () => {
    const result = await resolveProjectPath('relative/path', whitelist);
    expect('error' in result).toBe(true);
    if (!('error' in result)) return;
    expect(result.error.toLowerCase()).toContain('absolute');
  });

  it('rejects nonexistent paths', async () => {
    const result = await resolveProjectPath('/nonexistent/foo-boocode-test', whitelist);
    expect('error' in result).toBe(true);
    if (!('error' in result)) return;
    expect(result.error.toLowerCase()).toContain('does not exist');
  });

  it('rejects symlink escapes (realpath resolution catches traversal)', async () => {
    // Create a symlink INSIDE the whitelist that points OUTSIDE. realpath
    // should resolve through it and the resulting real path should fail the
    // whitelist scope check.
    const escapeLink = join(whitelist, 'escape-link');
    await symlink(outside, escapeLink);
    const result = await resolveProjectPath(escapeLink, whitelist);
    expect('error' in result).toBe(true);
    if (!('error' in result)) return;
    expect(result.error.toLowerCase()).toContain('path must be under');
  });

  it('rejects the whitelist directory itself as a project root', async () => {
    // A project's parent can't be the project. The scope check must require
    // the candidate path to be strictly below the whitelist (whitelist + sep
    // prefix), not just equal to it.
    const result = await resolveProjectPath(whitelist, whitelist);
    expect('error' in result).toBe(true);
    if (!('error' in result)) return;
    expect(result.error.toLowerCase()).toContain('path must be under');
  });

  it('rejects non-directory targets (file under whitelist)', async () => {
    const filePath = join(whitelist, 'a-file.txt');
    await writeFile(filePath, 'content', 'utf8');
    const result = await resolveProjectPath(filePath, whitelist);
    expect('error' in result).toBe(true);
    if (!('error' in result)) return;
    expect(result.error.toLowerCase()).toContain('not a directory');
  });
});
