import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { EventEmitter } from 'events';
import { PassThrough } from 'stream';
import fs from 'fs';
import path from 'path';

vi.mock('./channels/registry.js', () => ({ registerChannel: vi.fn() }));
vi.mock('../env.js', () => ({ readEnvFile: vi.fn(() => ({})) }));
vi.mock('./env.js', () => ({ readEnvFile: vi.fn(() => ({})) }));
vi.mock('./mount-security.js', () => ({ validateAdditionalMounts: vi.fn(() => []) }));
vi.mock('./logger.js', () => ({
  logger: {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}));
vi.mock('../logger.js', () => ({
  logger: {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}));
vi.mock('./config.js', () => ({
  ASSISTANT_NAME: 'Andy',
  TRIGGER_PATTERN: /^@Andy\b/i,
  CONTAINER_IMAGE: 'nanoclaw-agent:latest',
  CONTAINER_MAX_OUTPUT_SIZE: 10485760,
  CONTAINER_TIMEOUT: 1800000,
  DATA_DIR: '/tmp/nanoclaw-test-data',
  GROUPS_DIR: '/tmp/nanoclaw-test-groups',
  IDLE_TIMEOUT: 1800000,
  TIMEZONE: 'America/Los_Angeles',
  TENANT_HOME_BASE: '/tmp/nanoclaw-tenants',
}));

type Handler = (...args: any[]) => any;
const clientRef = vi.hoisted(() => ({ current: null as any }));

vi.mock('discord.js', () => {
  const Events = {
    MessageCreate: 'messageCreate',
    ClientReady: 'ready',
    Error: 'error',
    InteractionCreate: 'interactionCreate',
    GuildMemberAdd: 'guildMemberAdd',
    ChannelCreate: 'channelCreate',
  };

  const GatewayIntentBits = {
    Guilds: 1,
    GuildMessages: 2,
    MessageContent: 4,
    DirectMessages: 8,
    GuildMembers: 16,
  };

  const Partials = { Channel: 0, Message: 1 };

  class MockClient {
    eventHandlers = new Map<string, Handler[]>();
    user: any = { id: '999888777', tag: 'Andy#1234' };
    private _ready = false;

    constructor(_opts: any) {
      clientRef.current = this;
    }

    on(event: string, handler: Handler) {
      const existing = this.eventHandlers.get(event) || [];
      existing.push(handler);
      this.eventHandlers.set(event, existing);
      return this;
    }

    once(event: string, handler: Handler) {
      return this.on(event, handler);
    }

    guilds = {
      cache: Object.assign(new Map([['111222333444555', { id: '111222333444555' }]]), {
        first() { return { id: '111222333444555' }; },
      }),
    };

    async login(_token: string) {
      this._ready = true;
      const readyHandlers = this.eventHandlers.get('ready') || [];
      for (const h of readyHandlers) h({ user: this.user, guilds: this.guilds });
    }

    isReady() {
      return this._ready;
    }

    channels = {
      fetch: vi.fn().mockResolvedValue({
        send: vi.fn().mockResolvedValue(undefined),
        sendTyping: vi.fn().mockResolvedValue(undefined),
      }),
    };

    destroy() {
      this._ready = false;
    }
  }

  class TextChannel {}
  class AttachmentBuilder {}
  class SlashCommandBuilder {
    setName() { return this; }
    setDescription() { return this; }
    toJSON() { return {}; }
  }
  class REST {
    setToken() { return this; }
    put = vi.fn().mockResolvedValue(undefined);
  }
  const Routes = {
    applicationGuildCommands: vi.fn(() => '/guild-commands'),
    applicationCommands: vi.fn(() => '/global-commands'),
  };

  return {
    AttachmentBuilder,
    ChannelType: { GuildText: 0 },
    ChatInputCommandInteraction: class {},
    Client: MockClient,
    Events,
    GatewayIntentBits,
    GuildChannel: class {},
    Message: class {},
    Partials,
    REST,
    Routes,
    SlashCommandBuilder,
    TextChannel,
  };
});

function createFakeProcess() {
  const proc = new EventEmitter() as EventEmitter & {
    stdin: PassThrough;
    stdout: PassThrough;
    stderr: PassThrough;
    kill: ReturnType<typeof vi.fn>;
    pid: number;
  };
  proc.stdin = new PassThrough();
  proc.stdout = new PassThrough();
  proc.stderr = new PassThrough();
  proc.kill = vi.fn();
  proc.pid = 12345;
  return proc;
}

let fakeProc: ReturnType<typeof createFakeProcess>;
let whisperModelDir = '';

vi.mock('child_process', async () => {
  const actual = await vi.importActual<typeof import('child_process')>('child_process');
  return {
    ...actual,
    spawn: vi.fn(() => fakeProc),
    execSync: vi.fn(() => ''),
    execFileSync: vi.fn(() => ''),
    exec: vi.fn((_cmd: string, _opts: unknown, cb?: (err: Error | null) => void) => {
      if (cb) cb(null);
      return new EventEmitter();
    }),
  };
});

import { DiscordChannel } from './channels/discord.js';
import { runContainerAgent } from './container-runner.js';
import { spawn } from 'child_process';

const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';

function emitOutput(result: { status: 'success' | 'error'; result: string | null }) {
  fakeProc.stdout.push(`${OUTPUT_START_MARKER}\n${JSON.stringify(result)}\n${OUTPUT_END_MARKER}\n`);
}

function createMessage(attachments: Map<string, any>) {
  return {
    channelId: '1234567890123456',
    id: 'msg-audio-1',
    content: '<@999888777> summarize this voice note',
    createdAt: new Date('2024-01-01T00:00:00.000Z'),
    author: { id: '55512345', username: 'alice', displayName: 'Alice', bot: false },
    member: { displayName: 'Alice' },
    guild: { name: 'Server' },
    channel: {
      name: 'general',
      messages: {
        fetch: vi.fn().mockResolvedValue({
          author: { username: 'Bob', displayName: 'Bob' },
          member: { displayName: 'Bob' },
        }),
      },
    },
    mentions: {
      users: new Map([['999888777', { id: '999888777' }]]),
    },
    attachments,
    reference: null,
  };
}

async function triggerMessage(message: any) {
  const handlers = clientRef.current.eventHandlers.get('messageCreate') || [];
  for (const h of handlers) await h(message);
}

describe('audio attachment flow', () => {
  beforeEach(() => {
    fakeProc = createFakeProcess();
    vi.useFakeTimers();
    whisperModelDir = fs.mkdtempSync('/tmp/nanoclaw-whisper-models-');
    process.env.NANOCLAW_WHISPER_MODEL_FILE = path.join(whisperModelDir, 'ggml-base.en.bin');
    fs.writeFileSync(process.env.NANOCLAW_WHISPER_MODEL_FILE, 'fake-model');
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.unstubAllGlobals();
    delete process.env.NANOCLAW_WHISPER_MODEL_FILE;
    if (whisperModelDir) {
      fs.rmSync(whisperModelDir, { recursive: true, force: true });
      whisperModelDir = '';
    }
  });

  it('simulates Discord audio ingestion into a container run and response', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      arrayBuffer: async () => Uint8Array.from([1, 2, 3, 4]).buffer,
    }));

    const captured: any[] = [];
    const channel = new DiscordChannel('test-token', {
      onMessage: vi.fn((chatJid, msg) => captured.push({ chatJid, msg })),
      onChatMetadata: vi.fn(),
      registeredGroups: () => ({
        'dc:1234567890123456': {
          name: 'Server #general',
          folder: 'john/discord-general',
          added_at: '2024-01-01T00:00:00.000Z',
          tenantId: 'john',
        },
      }),
    });
    await channel.connect();

    const attachments = new Map([
      ['att1', { name: 'note.ogg', contentType: 'audio/ogg', url: 'https://cdn.example.test/note.ogg' }],
    ]);
    await triggerMessage(createMessage(attachments));

    expect(captured).toHaveLength(1);
    const inbound = captured[0].msg;
    expect(inbound.content).toBe('summarize this voice note');
    expect(inbound.attachments[0]).toEqual(
      expect.objectContaining({
        kind: 'audio',
        name: 'note.ogg',
        sourcePath: expect.any(String),
      }),
    );

    const sourcePath = inbound.attachments[0].sourcePath as string;
    expect(fs.existsSync(sourcePath)).toBe(true);

    let stdinPayload = '';
    fakeProc.stdin.on('data', (chunk) => {
      stdinPayload += chunk.toString();
    });

    const resultPromise = runContainerAgent(
      {
        name: 'Server #general',
        folder: 'john/discord-general',
        added_at: new Date().toISOString(),
        tenantId: 'john',
      },
      {
        prompt: `**Alice**: ${inbound.content}`,
        groupFolder: 'john/discord-general',
        chatJid: 'dc:1234567890123456',
        isMain: false,
        attachments: inbound.attachments,
      },
      () => {},
      undefined,
      {
        id: 'john',
        name: 'John',
        max_concurrent: 1,
        max_memory_mb: 512,
        cpu_quota: '50%',
        cpu_weight: 100,
        max_disk_mb: 1024,
        status: 'active',
        created_at: new Date().toISOString(),
      },
    );

    emitOutput({ status: 'success', result: 'Agent heard the audio transcript.' });
    await vi.advanceTimersByTimeAsync(10);
    fakeProc.emit('close', 0);
    await vi.advanceTimersByTimeAsync(10);

    const result = await resultPromise;
    expect(result.status).toBe('success');
    expect(result.result).toBe('Agent heard the audio transcript.');

    const args = (vi.mocked(spawn).mock.calls.at(-1)?.[1] || []) as string[];
    expect(args.some((arg) => arg.endsWith('/tmp/nanoclaw-tenants/john/nanoclaw/data/ipc/discord-general/attachments:/workspace/attachments:ro'))).toBe(true);
    expect(args.some((arg) => arg.endsWith(`${whisperModelDir}/ggml-base.en.bin:/models/whisper/ggml-base.en.bin:ro`))).toBe(true);

    const containerInput = JSON.parse(stdinPayload);
    expect(containerInput.attachments).toEqual([
      expect.objectContaining({
        kind: 'audio',
        mountedPath: expect.stringMatching(/^\/workspace\/attachments\/.+-note\.ogg$/),
      }),
    ]);

    fs.rmSync(path.dirname(sourcePath), { recursive: true, force: true });
  });
});
