import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, existsSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { resolveUserSubdir, ensureUserFolder, assertOwnerAccess, userRoot, readUserAgentsMd } from './paths.js';

describe('user-folder/paths', () => {
  let root: string;
  beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'uf-')); });
  afterEach(() => { rmSync(root, { recursive: true, force: true }); });

  it('creates the standard subdirs on first ensure', () => {
    ensureUserFolder(root, 'user-abc');
    for (const sub of ['scripts', 'browser-macros', 'templates', 'recordings', 'trash', 'memory', 'pets']) {
      expect(existsSync(join(root, 'user-abc', sub))).toBe(true);
    }
  });

  it('is idempotent — second call does not throw', () => {
    ensureUserFolder(root, 'user-abc');
    expect(() => ensureUserFolder(root, 'user-abc')).not.toThrow();
  });

  it('rejects empty ownerId', () => {
    expect(() => ensureUserFolder(root, '')).toThrow(/invalid ownerId/);
  });

  it('rejects ownerId containing path separators', () => {
    expect(() => ensureUserFolder(root, '../escape')).toThrow(/invalid ownerId/);
    expect(() => ensureUserFolder(root, 'a/b')).toThrow(/invalid ownerId/);
  });

  it('rejects absolute-path ownerId', () => {
    expect(() => ensureUserFolder(root, '/etc')).toThrow(/invalid ownerId/);
  });

  it('resolves subdir paths under the owner root', () => {
    const p = resolveUserSubdir(root, 'user-abc', 'scripts', 'foo.js');
    expect(p).toBe(join(root, 'user-abc', 'scripts', 'foo.js'));
  });

  it('rejects path traversal in the relative segment', () => {
    expect(() => resolveUserSubdir(root, 'user-abc', 'scripts', '../../etc/passwd'))
      .toThrow(/outside owner folder/);
  });

  it('rejects empty relPath', () => {
    expect(() => resolveUserSubdir(root, 'user-abc', 'scripts', ''))
      .toThrow(/relPath must not be empty/);
  });

  it('assertOwnerAccess passes when ctx.userId === ownerId', () => {
    expect(() => assertOwnerAccess({ userId: 'u1' }, 'u1')).not.toThrow();
  });

  it('assertOwnerAccess throws when ctx.userId is missing', () => {
    expect(() => assertOwnerAccess({}, 'u1')).toThrow(/unauthenticated/);
  });

  it('assertOwnerAccess throws on cross-user access', () => {
    expect(() => assertOwnerAccess({ userId: 'u1' }, 'u2')).toThrow(/forbidden/);
  });

  it('readUserAgentsMd returns null when file is missing', () => {
    ensureUserFolder(root, 'u1');
    expect(readUserAgentsMd(root, 'u1')).toBeNull();
  });

  it('readUserAgentsMd returns null for an empty file', () => {
    ensureUserFolder(root, 'u1');
    writeFileSync(join(userRoot(root, 'u1'), 'AGENTS.md'), '');
    expect(readUserAgentsMd(root, 'u1')).toBeNull();
  });

  it('readUserAgentsMd returns the file contents under the cap', () => {
    ensureUserFolder(root, 'u1');
    writeFileSync(join(userRoot(root, 'u1'), 'AGENTS.md'), 'hello world');
    expect(readUserAgentsMd(root, 'u1')).toBe('hello world');
  });

  it('readUserAgentsMd truncates files larger than 64 KB and appends a notice', () => {
    ensureUserFolder(root, 'u1');
    const big = Buffer.alloc(66 * 1024, 0x41); // 66 KB of 'A'
    writeFileSync(join(userRoot(root, 'u1'), 'AGENTS.md'), big);
    const result = readUserAgentsMd(root, 'u1')!;
    expect(result).toContain('[truncated: original was');
    expect(result.length).toBeLessThan(66 * 1024 + 100);
  });

  it('readUserAgentsMd returns null for invalid ownerId', () => {
    expect(readUserAgentsMd(root, '../escape')).toBeNull();
    expect(readUserAgentsMd(root, '')).toBeNull();
  });
});
