import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
  chmodSync,
  mkdirSync,
  mkdtempSync,
  renameSync,
  rmSync,
  symlinkSync,
  writeFileSync,
  type PathLike,
} from 'node:fs';
import type * as NodeFs from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

interface FilesystemRaceState {
  afterLstat?: (path: string) => void;
  afterOpen?: (path: string) => void;
}

const filesystemRaceState = vi.hoisted<FilesystemRaceState>(() => ({}));

vi.mock('node:fs', async (importOriginal) => {
  const actual = await importOriginal<typeof NodeFs>();
  return {
    ...actual,
    lstatSync: (path: PathLike) => {
      const result = actual.lstatSync(path);
      filesystemRaceState.afterLstat?.(String(path));
      return result;
    },
    openSync: (path: PathLike, flags: string | number, mode?: number) => {
      const fd = actual.openSync(path, flags, mode);
      filesystemRaceState.afterOpen?.(String(path));
      return fd;
    },
  };
});

import { assertCanonicalContainment, readRegularFileSecure } from './secure-file.js';

describe('secure file reads', () => {
  let root: string;

  beforeEach(() => {
    root = mkdtempSync(join(tmpdir(), 'mosaic-secure-file-'));
    filesystemRaceState.afterLstat = undefined;
    filesystemRaceState.afterOpen = undefined;
  });

  afterEach(() => {
    filesystemRaceState.afterLstat = undefined;
    filesystemRaceState.afterOpen = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('rejects canonical path escape', () => {
    expect(() => assertCanonicalContainment(root, join(root, '..', 'outside'))).toThrow(
      'path escapes managed root',
    );
  });

  it('rejects a symlink in a file ancestor', () => {
    const external = join(root, 'external');
    mkdirSync(external);
    writeFileSync(join(external, 'file'), 'external\n');
    symlinkSync(external, join(root, 'linked'));

    expect(() => readRegularFileSecure(join(root, 'linked', 'file'), { root })).toThrow(
      'path ancestor is a symbolic link',
    );
  });

  it('rejects a symlink target', () => {
    const external = join(root, 'external');
    writeFileSync(external, 'external\n');
    symlinkSync(external, join(root, 'linked-file'));

    expect(() => readRegularFileSecure(join(root, 'linked-file'), { root })).toThrow(
      'file is a symbolic link',
    );
  });

  it('keeps ancestor traversal bound when an opened directory is substituted', () => {
    const tools = join(root, 'tools');
    const displacedTools = join(root, 'tools.displaced');
    const external = join(root, 'external');
    const helper = join(tools, 'helper.sh');
    mkdirSync(tools);
    mkdirSync(external);
    writeFileSync(helper, 'trusted\n', { mode: 0o755 });
    writeFileSync(join(external, 'helper.sh'), 'external marker\n', { mode: 0o755 });

    let substituted = false;
    filesystemRaceState.afterOpen = (openedPath: string): void => {
      if (substituted || !openedPath.startsWith('/proc/self/fd/')) return;
      if (openedPath.split('/').at(-1) !== 'tools') return;
      substituted = true;
      renameSync(tools, displacedTools);
      symlinkSync(external, tools);
    };

    const result = readRegularFileSecure(helper, { root, executable: true });
    expect(substituted).toBe(true);
    expect(result.content.toString('utf8')).toBe('trusted\n');
  });

  it('keeps root selection bound when the opened root is substituted', () => {
    const displacedRoot = `${root}.displaced`;
    const externalRoot = `${root}.external`;
    const helper = join(root, 'helper.sh');
    mkdirSync(externalRoot);
    writeFileSync(helper, 'trusted root\n', { mode: 0o755 });
    writeFileSync(join(externalRoot, 'helper.sh'), 'external root marker\n', { mode: 0o755 });

    let substituted = false;
    filesystemRaceState.afterOpen = (openedPath: string): void => {
      if (substituted || !openedPath.startsWith('/proc/self/fd/')) return;
      const match = openedPath.match(/\/([^/]+)$/);
      if (match?.[1] !== root.split('/').filter(Boolean).at(-1)) return;
      substituted = true;
      renameSync(root, displacedRoot);
      symlinkSync(externalRoot, root);
    };

    const result = readRegularFileSecure(helper, { root, executable: true });
    expect(substituted).toBe(true);
    expect(result.content.toString('utf8')).toBe('trusted root\n');
    filesystemRaceState.afterOpen = undefined;
    rmSync(root);
    renameSync(displacedRoot, root);
  });

  it('keeps target read and execute validation bound to the opened file', () => {
    const file = join(root, 'helper.sh');
    const displaced = join(root, 'helper.displaced.sh');
    const external = join(root, 'external-helper.sh');
    writeFileSync(file, 'trusted target\n', { mode: 0o755 });
    writeFileSync(external, 'external target marker\n', { mode: 0o755 });

    let substituted = false;
    filesystemRaceState.afterOpen = (openedPath: string): void => {
      if (substituted || !openedPath.startsWith('/proc/self/fd/')) return;
      if (openedPath.split('/').at(-1) !== 'helper.sh') return;
      substituted = true;
      renameSync(file, displaced);
      symlinkSync(external, file);
    };

    const result = readRegularFileSecure(file, { root, executable: true });
    expect(substituted).toBe(true);
    expect(result.content.toString('utf8')).toBe('trusted target\n');
  });

  it('uses a stable redacted executable error while retaining the error code', () => {
    const file = join(root, 'helper.sh');
    writeFileSync(file, '#!/bin/sh\n', { mode: 0o644 });

    try {
      readRegularFileSecure(file, { root, executable: true });
      throw new Error('expected executable validation to fail');
    } catch (error) {
      expect(error).toMatchObject({ message: 'managed file is not executable', code: 'EACCES' });
      expect(String(error)).not.toContain('/proc/self/fd/');
      expect(String(error)).not.toContain(root);
    }
  });

  it('uses effective-identity execute access after regular-file validation', () => {
    const file = join(root, 'helper.sh');
    writeFileSync(file, '#!/bin/sh\n', { mode: 0o644 });
    expect(() => readRegularFileSecure(file, { root, executable: true })).toThrow();

    chmodSync(file, 0o755);
    expect(readRegularFileSecure(file, { root, executable: true }).content.toString()).toBe(
      '#!/bin/sh\n',
    );
  });
});
