import { Logger } from '@nestjs/common';
import { describe, expect, it, vi } from 'vitest';
import type { SlashCommandPayload, SystemReloadPayload } from '@mosaicstack/types';
import { ReloadService } from './reload.service.js';
import { CommandExecutorService } from '../commands/command-executor.service.js';

function createMockCommandRegistry() {
  return {
    getManifest: vi.fn().mockReturnValue({
      version: 1,
      commands: [],
      skills: [],
    }),
  };
}

function createService() {
  const registry = createMockCommandRegistry();
  const service = new ReloadService(registry as never);
  return { service, registry };
}

describe('ReloadService', () => {
  it('reload() calls onUnload then onLoad for registered MosaicPlugin', async () => {
    const { service } = createService();

    const callOrder: string[] = [];
    const mockPlugin = {
      pluginName: 'test-plugin',
      onLoad: vi.fn().mockImplementation(() => {
        callOrder.push('onLoad');
        return Promise.resolve();
      }),
      onUnload: vi.fn().mockImplementation(() => {
        callOrder.push('onUnload');
        return Promise.resolve();
      }),
    };

    service.registerPlugin('test-plugin', mockPlugin);
    const result = await service.reload('command');

    expect(mockPlugin.onUnload).toHaveBeenCalledOnce();
    expect(mockPlugin.onLoad).toHaveBeenCalledOnce();
    expect(callOrder).toEqual(['onUnload', 'onLoad']);
    expect(result.message).toContain('test-plugin');
  });

  it('reload() continues if one plugin throws during onUnload', async () => {
    const { service } = createService();

    const badPlugin = {
      pluginName: 'bad-plugin',
      onLoad: vi.fn().mockResolvedValue(undefined),
      onUnload: vi.fn().mockRejectedValue(new Error('unload failed')),
    };

    service.registerPlugin('bad-plugin', badPlugin);
    const result = await service.reload('command');

    expect(result.message).toContain('bad-plugin');
    expect(result.message).toContain('unload failed');
  });

  it('reload() skips non-MosaicPlugin objects', async () => {
    const { service } = createService();

    const notAPlugin = { foo: 'bar' };
    service.registerPlugin('not-a-plugin', notAPlugin);

    // Should not throw
    const result = await service.reload('command');
    expect(result).toBeDefined();
    expect(result.message).not.toContain('not-a-plugin');
  });

  it('reload() returns SystemReloadPayload with commands, skills, providers, message', async () => {
    const { service, registry } = createService();
    registry.getManifest.mockReturnValue({
      version: 1,
      commands: [
        {
          name: 'test',
          description: 'test cmd',
          aliases: [],
          scope: 'core',
          execution: 'socket',
          available: true,
        },
      ],
      skills: [],
    });

    const result = await service.reload('rest');

    expect(result).toHaveProperty('commands');
    expect(result).toHaveProperty('skills');
    expect(result).toHaveProperty('providers');
    expect(result).toHaveProperty('message');
    expect(result.commands).toHaveLength(1);
  });

  it('registerPlugin() logs plugin registration', () => {
    const { service } = createService();

    // Should not throw and should register
    expect(() => service.registerPlugin('my-plugin', {})).not.toThrow();
  });
});

describe('ReloadService — /reload command sanitizes plugin errors', () => {
  it('generic per-plugin errors reach the chat surface while raw markers stay server-side only', async () => {
    const registry = {
      getManifest: vi.fn().mockReturnValue({
        version: 1,
        commands: [
          { name: 'reload', aliases: [], scope: 'core', execution: 'socket', available: true },
        ],
        skills: [],
      }),
    };
    const reloadService = new ReloadService(registry as never);

    const RELOAD_LOAD_LEAK_MARKER = 'RELOAD_LOAD_LEAK_MARKER /srv/load-secret';
    const RELOAD_UNLOAD_LEAK_MARKER = 'RELOAD_UNLOAD_LEAK_MARKER /srv/unload-secret';

    reloadService.registerPlugin('unload-fails', {
      pluginName: 'unload-fails',
      onLoad: vi.fn().mockResolvedValue(undefined),
      onUnload: vi.fn().mockRejectedValue(new Error(RELOAD_UNLOAD_LEAK_MARKER)),
    });
    reloadService.registerPlugin('load-fails', {
      pluginName: 'load-fails',
      onLoad: vi.fn().mockRejectedValue(new Error(RELOAD_LOAD_LEAK_MARKER)),
      onUnload: vi.fn().mockResolvedValue(undefined),
    });

    const errorSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined);
    const broadcastReload = vi.fn();
    const mockChatGateway = { broadcastReload };
    const mockAgentService = { getSession: vi.fn(), applyAgentConfig: vi.fn() };
    const mockSystemOverride = { set: vi.fn(), get: vi.fn(), clear: vi.fn() };
    const mockSessionGC = { sweepOrphans: vi.fn() };
    const mockBrain = { agents: { findByName: vi.fn(), findById: vi.fn(), create: vi.fn() } };

    const mockMcpClient = {
      getServerStatuses: vi.fn(() => []),
      getToolDefinitions: vi.fn(() => []),
      reconnectServer: vi.fn().mockResolvedValue(undefined),
    };

    const executor = new CommandExecutorService(
      registry as never,
      mockAgentService as never,
      mockSystemOverride as never,
      mockSessionGC as never,
      null,
      mockBrain as never,
      reloadService,
      mockChatGateway as never,
      mockMcpClient as never,
    );

    const payload: SlashCommandPayload = { command: 'reload', conversationId: 'conv-1' };
    const result = await executor.execute(payload, { userId: 'user-1', tenantId: 'user-1' });

    expect(result.success).toBe(true);
    expect(result.message).toContain('unload-fails: unload failed (internal error)');
    expect(result.message).toContain('load-fails: load failed (internal error)');
    expect(result.message).not.toContain(RELOAD_UNLOAD_LEAK_MARKER);
    expect(result.message).not.toContain(RELOAD_LOAD_LEAK_MARKER);

    expect(broadcastReload).toHaveBeenCalledOnce();
    const broadcastPayload = broadcastReload.mock.calls[0]?.[0] as SystemReloadPayload;
    expect(broadcastPayload.message).toContain('unload-fails: unload failed (internal error)');
    expect(broadcastPayload.message).toContain('load-fails: load failed (internal error)');
    expect(broadcastPayload.message).not.toContain(RELOAD_UNLOAD_LEAK_MARKER);
    expect(broadcastPayload.message).not.toContain(RELOAD_LOAD_LEAK_MARKER);

    const loggedUnloadMarker = errorSpy.mock.calls.some((call) =>
      call.some((arg) => typeof arg === 'string' && arg.includes(RELOAD_UNLOAD_LEAK_MARKER)),
    );
    const loggedLoadMarker = errorSpy.mock.calls.some((call) =>
      call.some((arg) => typeof arg === 'string' && arg.includes(RELOAD_LOAD_LEAK_MARKER)),
    );
    expect(loggedUnloadMarker).toBe(true);
    expect(loggedLoadMarker).toBe(true);

    errorSpy.mockRestore();
  });
});
