import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CommandExecutorService } from './command-executor.service.js';
import type { SlashCommandPayload } from '@mosaicstack/types';

// Minimal mock implementations
const mockRegistry = {
  getManifest: vi.fn(() => ({
    version: 1,
    commands: [
      { name: 'provider', aliases: [], scope: 'agent', execution: 'hybrid', available: true },
      { name: 'mission', aliases: [], scope: 'agent', execution: 'socket', available: true },
      { name: 'agent', aliases: ['a'], scope: 'agent', execution: 'socket', available: true },
      { name: 'prdy', aliases: [], scope: 'agent', execution: 'socket', available: true },
      { name: 'tools', aliases: [], scope: 'agent', execution: 'socket', available: true },
    ],
    skills: [],
  })),
};

const mockAgentService = {
  getSession: vi.fn(() => undefined),
  applyAgentConfig: vi.fn(),
  updateSessionModel: vi.fn(),
};

const mockSystemOverride = {
  set: vi.fn(),
  get: vi.fn(),
  clear: vi.fn(),
  renew: vi.fn(),
};

const mockSessionGC = {
  sweepOrphans: vi.fn(() => ({ orphanedSessions: 0, totalCleaned: [], duration: 0 })),
};

const mockRedis = {
  set: vi.fn().mockResolvedValue('OK'),
  get: vi.fn(),
  del: vi.fn(),
};

// Mock agent config returned by brain.agents.findByName for "my-agent-id"
const mockAgentConfig = {
  id: 'my-agent-id',
  name: 'my-agent-id',
  model: 'claude-sonnet-4-6',
  provider: 'anthropic',
  systemPrompt: null,
  allowedTools: null,
  isSystem: false,
  ownerId: 'user-123',
  status: 'idle',
  createdAt: new Date(),
  updatedAt: new Date(),
};

const mockBrain = {
  agents: {
    // findByName resolves with the agent when name matches, undefined otherwise
    findByName: vi.fn((name: string) =>
      Promise.resolve(name === 'my-agent-id' ? mockAgentConfig : undefined),
    ),
    findById: vi.fn((id: string) =>
      Promise.resolve(id === 'my-agent-id' ? mockAgentConfig : undefined),
    ),
    create: vi.fn(),
  },
};

const mockChatGateway = {
  broadcastSessionInfo: vi.fn(),
};

function buildService(): CommandExecutorService {
  return new CommandExecutorService(
    mockRegistry as never,
    mockAgentService as never,
    mockSystemOverride as never,
    mockSessionGC as never,
    mockRedis as never,
    mockBrain as never,
    null,
    mockChatGateway as never,
    null,
  );
}

describe('CommandExecutorService — P8-012 commands', () => {
  let service: CommandExecutorService;
  const userId = 'user-123';
  const userScope = { userId, tenantId: userId };
  const conversationId = 'conv-456';

  beforeEach(() => {
    vi.clearAllMocks();
    service = buildService();
  });

  // /provider login — missing provider name
  it('/provider login with no provider name returns usage error', async () => {
    const payload: SlashCommandPayload = { command: 'provider', args: 'login', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(false);
    expect(result.message).toContain('Usage: /provider login');
    expect(result.command).toBe('provider');
  });

  // /provider login anthropic — no bearer token or auth URL reaches chat output
  it('/provider login <name> keeps its one-time token out of chat output', async () => {
    const payload: SlashCommandPayload = {
      command: 'provider',
      args: 'login anthropic',
      conversationId,
    };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.command).toBe('provider');
    expect(result.message).toContain('anthropic');
    expect(result.message).not.toContain('http');
    expect(result.message).not.toContain('token=');
    expect(result.data).toEqual({ provider: 'anthropic' });
    // Verify Valkey was called
    expect(mockRedis.set).toHaveBeenCalledOnce();
    const [key, value, , ttl] = mockRedis.set.mock.calls[0] as [string, string, string, number];
    expect(key).toContain('mosaic:auth:poll:');
    const stored = JSON.parse(value) as { status: string; provider: string; userId: string };
    expect(stored.status).toBe('pending');
    expect(stored.provider).toBe('anthropic');
    expect(stored.userId).toBe(userId);
    expect(ttl).toBe(300);
  });

  // /provider with no args — returns usage
  it('/provider with no args returns usage message', async () => {
    const payload: SlashCommandPayload = { command: 'provider', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.message).toContain('Usage: /provider');
  });

  // /provider list
  it('/provider list returns success', async () => {
    const payload: SlashCommandPayload = { command: 'provider', args: 'list', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.command).toBe('provider');
  });

  // /provider logout with no name — usage error
  it('/provider logout with no name returns error', async () => {
    const payload: SlashCommandPayload = { command: 'provider', args: 'logout', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(false);
    expect(result.message).toContain('Usage: /provider logout');
  });

  // /provider unknown subcommand
  it('/provider unknown subcommand returns error', async () => {
    const payload: SlashCommandPayload = {
      command: 'provider',
      args: 'unknown',
      conversationId,
    };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(false);
    expect(result.message).toContain('Unknown subcommand');
  });

  // /mission status
  it('/mission status returns stub message', async () => {
    const payload: SlashCommandPayload = { command: 'mission', args: 'status', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.command).toBe('mission');
    expect(result.message).toContain('Mission status');
  });

  // /mission with no args
  it('/mission with no args returns status stub', async () => {
    const payload: SlashCommandPayload = { command: 'mission', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.message).toContain('Mission status');
  });

  // /mission set <id>
  it('/mission set <id> returns confirmation', async () => {
    const payload: SlashCommandPayload = {
      command: 'mission',
      args: 'set my-mission-123',
      conversationId,
    };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.message).toContain('my-mission-123');
  });

  // /agent list
  it('/agent list returns stub message', async () => {
    const payload: SlashCommandPayload = { command: 'agent', args: 'list', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.command).toBe('agent');
    expect(result.message).toContain('agent');
  });

  // /agent with no args
  it('/agent with no args returns usage', async () => {
    const payload: SlashCommandPayload = { command: 'agent', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.message).toContain('Usage: /agent');
  });

  // /agent <id> — switch
  it('/agent <id> returns switch confirmation', async () => {
    const payload: SlashCommandPayload = {
      command: 'agent',
      args: 'my-agent-id',
      conversationId,
    };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.message).toContain('my-agent-id');
  });

  // /prdy
  it('/prdy returns PRD wizard message', async () => {
    const payload: SlashCommandPayload = { command: 'prdy', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.command).toBe('prdy');
    expect(result.message).toContain('mosaic prdy');
  });

  // /tools
  it('/tools returns tools stub message', async () => {
    const payload: SlashCommandPayload = { command: 'tools', conversationId };
    const result = await service.execute(payload, userScope);
    expect(result.success).toBe(true);
    expect(result.command).toBe('tools');
    expect(result.message).toContain('tools');
  });
});
