import { DataSource } from 'typeorm';
import { PluginInstance } from './entities/plugin-instance.entity';
import { PluginInstanceService, InstanceExistsError } from './plugin-instance.service';
import { AddIntegrationFabric1781900000000 } from '../../database/migrations/1781900000000-AddIntegrationFabric';
import type { PluginConfigSchema } from '../../core/plugins/plugin.interfaces';

describe('PluginInstanceService', () => {
  let ds: DataSource;
  let service: PluginInstanceService;
  beforeEach(async () => {
    ds = new DataSource({ type: 'better-sqlite3', database: ':memory:', entities: [PluginInstance], migrations: [] });
    await ds.initialize();
    const runner = ds.createQueryRunner();
    await new AddIntegrationFabric1781900000000().up(runner);
    await runner.release();
    service = new PluginInstanceService(ds.getRepository(PluginInstance));
  });
  afterEach(async () => {
    if (ds.isInitialized) await ds.destroy();
  });

  it('mints a 64-hex-char secret and stores a composite id', async () => {
    const inst = await service.mint('chatwoot', 'acct1', { sessionScope: 'sess-1' });
    expect(inst.id).toBe('chatwoot:acct1');
    expect(inst.secret).toMatch(/^[0-9a-f]{64}$/);
  });

  it('masks the secret on the operator-facing view', async () => {
    const inst = await service.mint('chatwoot', 'acct1', {});
    expect(service.maskedView(inst).secret).toBe('***');
  });

  it('masks secret:true config fields (apiToken) on masked reads', () => {
    const schema = {
      type: 'object',
      properties: { apiToken: { type: 'string', secret: true }, accountId: { type: 'number' } },
    } as PluginConfigSchema;
    const inst = {
      id: 'p:i',
      secret: 'x',
      config: { apiToken: 'live-token', accountId: 3 },
    } as unknown as PluginInstance;
    const masked = service.maskedView(inst, schema);
    const config = masked.config as Record<string, unknown>;
    expect(masked.secret).toBe('***');
    expect(config.apiToken).toBe('***');
    expect(config.accountId).toBe(3);
  });

  it('masks the ENTIRE config when the schema is unavailable, e.g. the plugin is unloaded (fail-closed)', () => {
    const inst = {
      id: 'p:i',
      secret: 'x',
      config: { apiToken: 'live-token', accountId: 3 },
    } as unknown as PluginInstance;
    const config = service.maskedView(inst, undefined).config as Record<string, unknown>;
    expect(config.apiToken).toBe('***');
    expect(config.accountId).toBe('***');
  });

  it('resolves an existing instance and returns null for an unknown one', async () => {
    await service.mint('chatwoot', 'acct1', {});
    expect((await service.resolve('chatwoot', 'acct1'))?.id).toBe('chatwoot:acct1');
    expect(await service.resolve('chatwoot', 'nope')).toBeNull();
  });

  it('accepts a valid operator secret, rejects short/empty, else auto-generates', async () => {
    const ok = await service.create('chatwoot-adapter', 'a1', { secret: 'cw-secret-16chars!' });
    expect(ok.secret).toBe('cw-secret-16chars!');
    await expect(service.create('chatwoot-adapter', 'a2', { secret: '   ' })).rejects.toThrow(/secret/i);
    await expect(service.create('chatwoot-adapter', 'a3', { secret: 'short' })).rejects.toThrow(/16/);
    const gen = await service.create('chatwoot-adapter', 'a4', {});
    expect(gen.secret).toMatch(/^[0-9a-f]{64}$/);
  });

  it('masks a NESTED secret:true config field on masked reads (recursive redaction)', () => {
    const schema = {
      type: 'object',
      properties: {
        provider: {
          type: 'object',
          properties: { apiToken: { type: 'string', secret: true }, region: { type: 'string' } },
        },
      },
    } as PluginConfigSchema;
    const inst = {
      id: 'p:i',
      secret: 'x',
      config: { provider: { apiToken: 'live-token', region: 'us' } },
    } as unknown as PluginInstance;
    const config = service.maskedView(inst, schema).config as { provider: Record<string, unknown> };
    expect(config.provider.apiToken).toBe('***');
    expect(config.provider.region).toBe('us');
  });

  it('update restores a masked (sentinel) secret to the stored value instead of persisting "***"', async () => {
    const schema = {
      type: 'object',
      properties: { apiToken: { type: 'string', secret: true }, accountId: { type: 'number' } },
    } as PluginConfigSchema;
    await service.create('chatwoot', 'acct1', { config: { apiToken: 'real-token', accountId: 1 } });

    // Dashboard round-trips the masked config back with an edited non-secret field.
    const updated = await service.update('chatwoot', 'acct1', { config: { apiToken: '***', accountId: 2 } }, schema);

    expect(updated?.config).toEqual({ apiToken: 'real-token', accountId: 2 });
  });
});

describe('PluginInstanceService provisioning', () => {
  let ds: DataSource;
  let service: PluginInstanceService;
  beforeEach(async () => {
    ds = new DataSource({ type: 'better-sqlite3', database: ':memory:', entities: [PluginInstance], migrations: [] });
    await ds.initialize();
    const runner = ds.createQueryRunner();
    await new AddIntegrationFabric1781900000000().up(runner);
    await runner.release();
    service = new PluginInstanceService(ds.getRepository(PluginInstance));
  });
  afterEach(async () => {
    if (ds.isInitialized) await ds.destroy();
  });

  it('create mints a new instance and rejects a duplicate with InstanceExistsError', async () => {
    const inst = await service.create('chatwoot', 'acct1', { sessionScope: 'sess-1' });
    expect(inst.id).toBe('chatwoot:acct1');
    expect(inst.secret).toMatch(/^[0-9a-f]{64}$/);
    await expect(service.create('chatwoot', 'acct1', {})).rejects.toBeInstanceOf(InstanceExistsError);
  });

  it('list returns all instances for a plugin', async () => {
    await service.create('chatwoot', 'acct1', {});
    await service.create('chatwoot', 'acct2', {});
    await service.create('other', 'x', {});
    const list = await service.list('chatwoot');
    expect(list.map(i => i.instanceId).sort()).toEqual(['acct1', 'acct2']);
  });

  it('regenerateSecret replaces the secret with a new value', async () => {
    const created = await service.create('chatwoot', 'acct1', {});
    const rotated = await service.regenerateSecret('chatwoot', 'acct1');
    expect(rotated.secret).toMatch(/^[0-9a-f]{64}$/);
    expect(rotated.secret).not.toBe(created.secret);
  });

  it('setEnabled toggles enabled; update patches scope/config; remove deletes', async () => {
    await service.create('chatwoot', 'acct1', { sessionScope: 'a' });
    expect((await service.setEnabled('chatwoot', 'acct1', false))?.enabled).toBe(false);
    const patched = await service.update('chatwoot', 'acct1', { sessionScope: 'b', config: { k: 1 } });
    expect(patched?.sessionScope).toBe('b');
    expect(patched?.config).toEqual({ k: 1 });
    expect(await service.remove('chatwoot', 'acct1')).toBe(true);
    expect(await service.resolve('chatwoot', 'acct1')).toBeNull();
    expect(await service.remove('chatwoot', 'acct1')).toBe(false);
  });

  it('normalizes an empty sessionScope to null (all-sessions) on mint/create/update, never a literal ""', async () => {
    // '' would break outbound send (falsy sessionId) and be unrecoverable from the UI; store null instead.
    const minted = await service.mint('chatwoot', 'm1', { sessionScope: '' });
    expect(minted.sessionScope).toBeNull();
    const created = await service.create('chatwoot', 'acct1', { sessionScope: '' });
    expect(created.sessionScope).toBeNull();
    const patched = await service.update('chatwoot', 'acct1', { sessionScope: '' });
    expect(patched?.sessionScope).toBeNull();
  });
});
