import fs from 'fs';
import os from 'os';
import path from 'path';

import { afterEach, describe, expect, it } from 'vitest';

import {
  AGENTS_FILENAME,
  BOOTSTRAP_FILENAME,
  CLAUDE_COMPAT_FILENAME,
  IDENTITY_FILENAME,
  MEMORY_DIRNAME,
  MEMORY_FILENAME,
  SOUL_FILENAME,
  USER_FILENAME,
  buildWorkspaceContextAppend,
  ensureWorkspaceContract,
} from './workspace-contract.js';

const tempDirs: string[] = [];

function makeTempDir(): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nanoclaw-workspace-contract-'));
  tempDirs.push(dir);
  return dir;
}

afterEach(() => {
  while (tempDirs.length > 0) {
    fs.rmSync(tempDirs.pop()!, { recursive: true, force: true });
  }
});

describe('ensureWorkspaceContract', () => {
  it('creates canonical files and CLAUDE.md compatibility symlink for a main group', () => {
    const dir = makeTempDir();
    ensureWorkspaceContract(dir, {
      createDefaults: true,
      defaults: {
        [AGENTS_FILENAME]: '# Agents\n',
        [SOUL_FILENAME]: '# Soul\n',
        [IDENTITY_FILENAME]: '# Identity\n',
        [USER_FILENAME]: '# User\n',
        [MEMORY_FILENAME]: '# Memory\n',
        [BOOTSTRAP_FILENAME]: '# Bootstrap\n',
      },
    });

    expect(fs.existsSync(path.join(dir, AGENTS_FILENAME))).toBe(true);
    expect(fs.existsSync(path.join(dir, SOUL_FILENAME))).toBe(true);
    expect(fs.existsSync(path.join(dir, IDENTITY_FILENAME))).toBe(true);
    expect(fs.existsSync(path.join(dir, USER_FILENAME))).toBe(true);
    expect(fs.existsSync(path.join(dir, MEMORY_FILENAME))).toBe(true);
    expect(fs.existsSync(path.join(dir, BOOTSTRAP_FILENAME))).toBe(true);
    expect(fs.existsSync(path.join(dir, MEMORY_DIRNAME))).toBe(true);

    const claudePath = path.join(dir, CLAUDE_COMPAT_FILENAME);
    expect(fs.lstatSync(claudePath).isSymbolicLink()).toBe(true);
    expect(fs.readlinkSync(claudePath)).toBe(MEMORY_FILENAME);
  });

  it('creates local inherited-reference files for non-main groups', () => {
    const mainDir = makeTempDir();
    const groupDir = makeTempDir();

    ensureWorkspaceContract(mainDir, {
      createDefaults: true,
      defaults: {
        [AGENTS_FILENAME]: '# Main Agents\n',
        [SOUL_FILENAME]: '# Main Soul\n',
        [IDENTITY_FILENAME]: '# Main Identity\n',
        [USER_FILENAME]: '# Main User\n',
        [MEMORY_FILENAME]: '# Main Memory\n',
        [BOOTSTRAP_FILENAME]: '# Main Bootstrap\n',
      },
    });

    ensureWorkspaceContract(groupDir, { mainGroupDir: mainDir });

    for (const fileName of [AGENTS_FILENAME, SOUL_FILENAME, IDENTITY_FILENAME, USER_FILENAME]) {
      const filePath = path.join(groupDir, fileName);
      expect(fs.lstatSync(filePath).isSymbolicLink()).toBe(false);
      expect(fs.readFileSync(filePath, 'utf-8')).toContain(
        `Read /workspace/main-profile/${fileName} before using or editing this file.`,
      );
    }
    expect(fs.existsSync(path.join(groupDir, BOOTSTRAP_FILENAME))).toBe(false);
    expect(fs.readlinkSync(path.join(groupDir, CLAUDE_COMPAT_FILENAME))).toBe(MEMORY_FILENAME);
    expect(fs.existsSync(path.join(groupDir, MEMORY_FILENAME))).toBe(true);
  });

  it('migrates inherited profile symlinks to local reference files', () => {
    const mainDir = makeTempDir();
    const groupDir = makeTempDir();

    ensureWorkspaceContract(mainDir, {
      createDefaults: true,
      defaults: {
        [AGENTS_FILENAME]: '# Main Agents\n',
        [SOUL_FILENAME]: '# Main Soul\n',
        [IDENTITY_FILENAME]: '# Main Identity\n',
        [USER_FILENAME]: '# Main User\n',
        [MEMORY_FILENAME]: '# Main Memory\n',
        [BOOTSTRAP_FILENAME]: '# Main Bootstrap\n',
      },
    });

    for (const fileName of [
      AGENTS_FILENAME,
      SOUL_FILENAME,
      IDENTITY_FILENAME,
      USER_FILENAME,
    ]) {
      fs.symlinkSync(path.join(mainDir, fileName), path.join(groupDir, fileName));
    }

    ensureWorkspaceContract(groupDir, { mainGroupDir: mainDir });

    for (const fileName of [AGENTS_FILENAME, SOUL_FILENAME, IDENTITY_FILENAME, USER_FILENAME]) {
      const filePath = path.join(groupDir, fileName);
      expect(fs.lstatSync(filePath).isSymbolicLink()).toBe(false);
      expect(fs.readFileSync(filePath, 'utf-8')).toContain(
        `/workspace/main-profile/${fileName}`,
      );
    }
    expect(fs.existsSync(path.join(groupDir, BOOTSTRAP_FILENAME))).toBe(false);
  });

  it('migrates a legacy CLAUDE.md file into MEMORY.md and preserves a backup', () => {
    const dir = makeTempDir();
    fs.writeFileSync(path.join(dir, CLAUDE_COMPAT_FILENAME), 'Legacy memory\n');

    ensureWorkspaceContract(dir);

    expect(fs.readFileSync(path.join(dir, MEMORY_FILENAME), 'utf-8')).toContain('Legacy memory');
    expect(fs.existsSync(path.join(dir, `${CLAUDE_COMPAT_FILENAME}.legacy`))).toBe(true);
    expect(fs.lstatSync(path.join(dir, CLAUDE_COMPAT_FILENAME)).isSymbolicLink()).toBe(true);
  });
});

describe('buildWorkspaceContextAppend', () => {
  it('injects canonical workspace files but not MEMORY.md', () => {
    const dir = makeTempDir();
    fs.writeFileSync(path.join(dir, AGENTS_FILENAME), '# Agents\n');
    fs.writeFileSync(path.join(dir, SOUL_FILENAME), '# Soul\n');
    fs.writeFileSync(path.join(dir, IDENTITY_FILENAME), '# Identity\n');
    fs.writeFileSync(path.join(dir, USER_FILENAME), '# User\n');
    fs.writeFileSync(path.join(dir, BOOTSTRAP_FILENAME), '# Bootstrap\n');
    fs.writeFileSync(path.join(dir, MEMORY_FILENAME), '# Memory\n');

    const result = buildWorkspaceContextAppend(dir);

    expect(result.text).toContain('<file name="AGENTS.md">');
    expect(result.text).toContain('<file name="SOUL.md">');
    expect(result.text).toContain('<file name="IDENTITY.md">');
    expect(result.text).toContain('<file name="USER.md">');
    expect(result.text).toContain('<file name="BOOTSTRAP.md">');
    expect(result.text).not.toContain('<file name="MEMORY.md">');
  });

  it('includes guidance to keep persistence confirmations file-agnostic', () => {
    const dir = makeTempDir();
    fs.writeFileSync(path.join(dir, AGENTS_FILENAME), '# Agents\n');

    const result = buildWorkspaceContextAppend(dir);

    expect(result.text).toContain(
      'When you confirm a memory/profile/onboarding update to the user, describe the outcome plainly and do not mention workspace file names or paths unless the user explicitly asks where it was stored.',
    );
  });

  it('includes the hosted-app completion rule', () => {
    const dir = makeTempDir();
    fs.writeFileSync(path.join(dir, AGENTS_FILENAME), '# Agents\n');

    const result = buildWorkspaceContextAppend(dir);

    expect(result.text).toContain(
      'If a user asks to build, create, scaffold, run, deploy, host, or expose an app, site, frontend, backend, or API, treat that as a hosted-app request by default.',
    );
    expect(result.text).toContain(
      'Hosted-app work is not complete until source is under /workspace/apps/<app>/, app.yml exists, app-apply has been sent, and the real public URL has been verified.',
    );
    expect(result.text).toContain(
      'Do not stop at local source files or tell the user to run the app themselves with npm start, node, Docker Compose, or similar handoff steps unless they explicitly asked for local-only work.',
    );
  });

  it('truncates injected content to configured budgets', () => {
    const dir = makeTempDir();
    fs.writeFileSync(path.join(dir, AGENTS_FILENAME), 'A'.repeat(50));

    const result = buildWorkspaceContextAppend(dir, { maxFileChars: 10, maxTotalChars: 10 });

    expect(result.text).toContain('truncated="true"');
    expect(result.files.find((file) => file.name === AGENTS_FILENAME)?.injectedChars).toBe(10);
  });
});
