import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, test } from 'vitest';

import { assertContainedRealPath } from '../../src/path-containment';

let dir: string;

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

describe('assertContainedRealPath', () => {
  test('allows a normal file inside the root', async () => {
    dir = await mkdtemp(path.join(tmpdir(), 'spoon-'));
    const root = path.join(dir, 'repo');
    await mkdir(root, { recursive: true });
    await writeFile(path.join(root, 'a.txt'), 'hi');
    await expect(assertContainedRealPath(root, 'a.txt')).resolves.toBe(
      path.join(root, 'a.txt'),
    );
  });

  test('rejects a lexical .. escape', async () => {
    dir = await mkdtemp(path.join(tmpdir(), 'spoon-'));
    const root = path.join(dir, 'repo');
    await mkdir(root, { recursive: true });
    await expect(assertContainedRealPath(root, '../secret')).rejects.toThrow();
  });

  test('rejects reading through a symlink that escapes the root', async () => {
    dir = await mkdtemp(path.join(tmpdir(), 'spoon-'));
    const root = path.join(dir, 'repo');
    const outside = path.join(dir, 'outside');
    await mkdir(root, { recursive: true });
    await mkdir(outside, { recursive: true });
    await writeFile(path.join(outside, 'secret'), 'S');
    await symlink(outside, path.join(root, 'link'));
    await expect(
      assertContainedRealPath(root, 'link/secret'),
    ).rejects.toThrow();
  });

  test('rejects a write whose final target is a symlink', async () => {
    dir = await mkdtemp(path.join(tmpdir(), 'spoon-'));
    const root = path.join(dir, 'repo');
    await mkdir(root, { recursive: true });
    await symlink(path.join(dir, 'evil'), path.join(root, 'out'));
    await expect(
      assertContainedRealPath(root, 'out', { forWrite: true }),
    ).rejects.toThrow();
  });
});
