import { ConfigService } from '@nestjs/config';
import { ModuleRef } from '@nestjs/core';
import { PluginLoaderService } from './plugin-loader.service';
import { PluginStorageService } from './plugin-storage.service';
import { HookManager, HookHandler } from '../hooks';
import { PluginContext, PluginInstance, PluginManifest, PluginStatus, PluginType } from './plugin.interfaces';

function makePlugin(opts: { sessionScoped?: boolean; activeSessions?: string[] }): PluginInstance {
  const manifest: PluginManifest = {
    id: 'act-ext',
    name: 'Activation Ext',
    version: '1.0.0',
    type: PluginType.EXTENSION,
    main: 'index.js',
    sessionScoped: opts.sessionScoped,
  };
  return { manifest, status: PluginStatus.ENABLED, config: {}, instance: null, activeSessions: opts.activeSessions };
}

describe('PluginLoaderService — per-session activation gate (hook delivery)', () => {
  let loader: PluginLoaderService;
  let hookManager: HookManager;
  let setSessions: jest.Mock;

  beforeEach(() => {
    hookManager = new HookManager();
    const configService = { get: jest.fn().mockReturnValue(undefined) } as unknown as ConfigService;
    setSessions = jest.fn();
    const pluginStorage = {
      createPluginStorage: jest.fn().mockReturnValue({}),
      setPluginSessions: setSessions,
    } as unknown as PluginStorageService;
    loader = new PluginLoaderService(configService, hookManager, pluginStorage, {
      get: jest.fn(),
    } as unknown as ModuleRef);
  });

  const seed = (plugin: PluginInstance): void => {
    (loader as unknown as { plugins: Map<string, PluginInstance> }).plugins.set(plugin.manifest.id, plugin);
  };

  function register(plugin: PluginInstance, handler: HookHandler): void {
    const ctx = (
      loader as unknown as { capabilities: { createPluginContext: (p: PluginInstance) => PluginContext } }
    ).capabilities.createPluginContext(plugin);
    ctx.registerHook('message:received', handler);
  }

  const fire = (sessionId: string): Promise<unknown> =>
    hookManager.execute('message:received', {}, { sessionId, source: 'Engine' });

  it('delivers a hook only for the sessions a session-scoped plugin is activated for', async () => {
    const handler = jest.fn().mockResolvedValue({ continue: true });
    register(makePlugin({ activeSessions: ['sess-1'] }), handler);

    await fire('sess-1');
    expect(handler).toHaveBeenCalledTimes(1);

    await fire('sess-2');
    expect(handler).toHaveBeenCalledTimes(1); // not delivered for the inactive session
  });

  it("delivers for every session when activeSessions is ['*']", async () => {
    const handler = jest.fn().mockResolvedValue({ continue: true });
    register(makePlugin({ activeSessions: ['*'] }), handler);

    await fire('a');
    await fire('b');
    expect(handler).toHaveBeenCalledTimes(2);
  });

  it('defaults to all sessions when activeSessions is unset', async () => {
    const handler = jest.fn().mockResolvedValue({ continue: true });
    register(makePlugin({}), handler); // no activeSessions

    await fire('anything');
    expect(handler).toHaveBeenCalledTimes(1);
  });

  it('a global plugin (sessionScoped:false) always receives the hook, even with no active sessions', async () => {
    const handler = jest.fn().mockResolvedValue({ continue: true });
    register(makePlugin({ sessionScoped: false, activeSessions: [] }), handler);

    await fire('x');
    expect(handler).toHaveBeenCalledTimes(1);
  });

  describe('setPluginSessions', () => {
    it('updates a session-scoped plugin and persists the new active set', () => {
      const plugin = makePlugin({ activeSessions: ['*'] });
      seed(plugin);

      loader.setPluginSessions('act-ext', ['sess-1', 'sess-2']);

      expect(plugin.activeSessions).toEqual(['sess-1', 'sess-2']);
      expect(setSessions).toHaveBeenCalledWith('act-ext', ['sess-1', 'sess-2']);
    });

    it('rejects activating a global (non-session-scoped) plugin per session', () => {
      seed(makePlugin({ sessionScoped: false }));
      expect(() => loader.setPluginSessions('act-ext', ['sess-1'])).toThrow(/global/i);
      expect(setSessions).not.toHaveBeenCalled();
    });
  });
});

// A built-in/bundled plugin must read its persisted per-session activation + config back into the
// runtime on boot, exactly like a directory plugin (loadPlugin) — otherwise the gate falls back to
// all-sessions/base-config after every restart, silently widening delivery for a plugin the operator
// had restricted.
describe('PluginLoaderService — registerBuiltInPlugin restart read-back', () => {
  it('seeds activeSessions and sessionConfig from persisted storage', () => {
    const hookManager = new HookManager();
    const configService = { get: jest.fn().mockReturnValue(undefined) } as unknown as ConfigService;
    const pluginStorage = {
      getPluginConfig: jest.fn().mockReturnValue({}),
      getPluginSessions: jest.fn().mockReturnValue(['sess-1']),
      getPluginSessionConfig: jest.fn().mockReturnValue({ 'sess-1': { lang: 'id' } }),
      getPluginEntry: jest.fn().mockReturnValue(undefined),
      setPluginEntry: jest.fn(),
    } as unknown as PluginStorageService;
    const loader = new PluginLoaderService(configService, hookManager, pluginStorage, {
      get: jest.fn(),
    } as unknown as ModuleRef);

    const manifest: PluginManifest = {
      id: 'builtin-ext',
      name: 'Built-in Ext',
      version: '1.0.0',
      type: PluginType.EXTENSION,
      main: 'index.js',
      sessionScoped: true,
    };
    loader.registerBuiltInPlugin(manifest, {});

    const plugin = (loader as unknown as { plugins: Map<string, PluginInstance> }).plugins.get('builtin-ext');
    expect(plugin?.activeSessions).toEqual(['sess-1']);
    expect(plugin?.sessionConfig).toEqual({ 'sess-1': { lang: 'id' } });
  });
});
