// v1.13.17-cross-repo-reads: pathGuard now accepts an optional extraRoots
// list. Validates the primary-root path stays the source of truth and that
// extra roots are consulted when (and only when) the primary rejects.

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

let tmp: string;
let projectRoot: string;
let altRoot: string;
let outsideDir: string;

beforeAll(async () => {
  tmp = await realpath(await mkdtemp(join(tmpdir(), 'boocode-pg-')));
  projectRoot = join(tmp, 'project');
  altRoot = join(tmp, 'alt');
  outsideDir = join(tmp, 'outside');
  await mkdir(projectRoot, { recursive: true });
  await mkdir(altRoot, { recursive: true });
  await mkdir(outsideDir, { recursive: true });
  await writeFile(join(projectRoot, 'inside.txt'), 'p');
  await writeFile(join(altRoot, 'cross.txt'), 'a');
  await writeFile(join(outsideDir, 'forbidden.txt'), 'x');
});

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

describe('pathGuard (v1.13.17 extraRoots)', () => {
  it('accepts paths inside the primary projectRoot', async () => {
    const real = await pathGuard(projectRoot, 'inside.txt');
    expect(real).toBe(join(projectRoot, 'inside.txt'));
  });

  it('rejects paths outside the primary root when no extra roots given', async () => {
    await expect(pathGuard(projectRoot, join(outsideDir, 'forbidden.txt'))).rejects.toBeInstanceOf(
      PathScopeError,
    );
  });

  it('accepts cross-root paths when the matching extra root is provided', async () => {
    const real = await pathGuard(projectRoot, join(altRoot, 'cross.txt'), [altRoot]);
    expect(real).toBe(join(altRoot, 'cross.txt'));
  });

  it('rejects cross-root paths even with extra roots when no root matches', async () => {
    await expect(
      pathGuard(projectRoot, join(outsideDir, 'forbidden.txt'), [altRoot]),
    ).rejects.toBeInstanceOf(PathScopeError);
  });

  it('ignores empty-string extra roots silently', async () => {
    const real = await pathGuard(projectRoot, join(altRoot, 'cross.txt'), ['', altRoot]);
    expect(real).toBe(join(altRoot, 'cross.txt'));
  });

  it('error message contains the request_read_access hint when scope rejects', async () => {
    try {
      await pathGuard(projectRoot, join(outsideDir, 'forbidden.txt'));
      throw new Error('should have thrown');
    } catch (err) {
      expect(err).toBeInstanceOf(PathScopeError);
      expect((err as Error).message).toContain('request_read_access');
    }
  });

  it('still resolves symlinks before the scope check', async () => {
    const linkPath = join(projectRoot, 'link-to-outside');
    await symlink(join(outsideDir, 'forbidden.txt'), linkPath);
    // Symlink target escapes both primary and the single extra root, so
    // even though the surface path "looks" inside projectRoot, the real
    // path resolves outside and the guard rejects.
    await expect(pathGuard(projectRoot, linkPath, [altRoot])).rejects.toBeInstanceOf(
      PathScopeError,
    );
    // But adding outsideDir as an extra root accepts (realpath inside it).
    const real = await pathGuard(projectRoot, linkPath, [altRoot, outsideDir]);
    expect(real).toBe(join(outsideDir, 'forbidden.txt'));
  });

  it('tries extra roots in order until one accepts', async () => {
    const real = await pathGuard(projectRoot, join(altRoot, 'cross.txt'), [
      outsideDir, // rejects
      altRoot,    // accepts
    ]);
    expect(real).toBe(join(altRoot, 'cross.txt'));
  });
});
