import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ConfigService } from '@nestjs/config';
import { createLogger } from '../../common/services/logger.service';
import { PluginStorageService } from './plugin-storage.service';
import { PluginUninstaller } from './plugin-uninstaller';
import { PluginInstance, PluginStatus, PluginType } from './plugin.interfaces';

/**
 * The uninstall job, on the collaborator that owns it since the loader split. Construction only is
 * adapted from the loader-level specs these tests moved from: instead of loading plugins through the
 * boot scan, the shared registry Map and persisted entries are seeded directly — the inputs
 * uninstallPlugin actually reads. The unload seam is the callback the lifecycle would provide; every
 * assertion is unchanged.
 */
const makeUninstaller = (config: ConfigService, storage: PluginStorageService, pluginsDir: string) => {
  const plugins = new Map<string, PluginInstance>();
  const uninstaller = new PluginUninstaller(
    createLogger('PluginLoaderService'),
    plugins,
    storage,
    pluginsDir,
    // The lifecycle's unload drops the runtime record; that is all uninstall needs from it.
    pluginId => {
      plugins.delete(pluginId);
      return Promise.resolve();
    },
  );
  return { uninstaller, plugins };
};

/** The registry record a boot-scan load would have left for an installed user plugin. */
const loadedUserPlugin = (id: string, packageDir: string): PluginInstance => ({
  manifest: { id, name: id, version: '1.0.0', type: PluginType.EXTENSION, main: 'index.js' },
  status: PluginStatus.INSTALLED,
  config: {},
  instance: null,
  loadedAt: new Date(),
  builtIn: false,
  packageDir,
});

const seedEntry = (storage: PluginStorageService, id: string, builtIn = false): void => {
  storage.setPluginEntry({
    id,
    type: 'extension',
    name: id,
    version: '1.0.0',
    status: 'installed',
    config: {},
    builtIn,
    installedAt: new Date(),
    updatedAt: new Date(),
  } as never);
};

describe('PluginUninstaller — uninstall', () => {
  let tmpDir: string;
  let pluginsDir: string;
  let storage: PluginStorageService;
  let uninstaller: PluginUninstaller;
  let plugins: Map<string, PluginInstance>;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owa-uninst-'));
    pluginsDir = path.join(tmpDir, 'plugins');
    fs.mkdirSync(pluginsDir, { recursive: true });
    const config = {
      get: (k: string) => (k === 'plugins.dir' ? pluginsDir : k === 'dataDir' ? tmpDir : undefined),
    } as unknown as ConfigService;
    storage = new PluginStorageService(config);
    ({ uninstaller, plugins } = makeUninstaller(config, storage, pluginsDir));
  });
  afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));

  const writeUserPlugin = (id: string): string => {
    const dir = path.join(pluginsDir, id);
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(
      path.join(dir, 'manifest.json'),
      JSON.stringify({ id, name: id, version: '1.0.0', type: 'extension', main: 'index.js' }),
    );
    fs.writeFileSync(path.join(dir, 'index.js'), 'module.exports = class {};');
    return dir;
  };

  it('removes the plugin directory, registry entry, and runtime instance', async () => {
    const dir = writeUserPlugin('user-plg');
    plugins.set('user-plg', loadedUserPlugin('user-plg', dir));
    seedEntry(storage, 'user-plg');
    expect(storage.getPluginEntry('user-plg')).toBeDefined();

    await uninstaller.uninstallPlugin('user-plg');

    expect(fs.existsSync(dir)).toBe(false);
    expect(storage.getPluginEntry('user-plg')).toBeUndefined();
    expect(plugins.get('user-plg')).toBeUndefined();
  });

  it('refuses to uninstall a built-in plugin', async () => {
    plugins.set('core-engine', { ...loadedUserPlugin('core-engine', pluginsDir), builtIn: true });
    seedEntry(storage, 'core-engine', true);
    await expect(uninstaller.uninstallPlugin('core-engine')).rejects.toThrow(/built-in/i);
  });
});

describe('PluginUninstaller — uninstall cleans up split-dir plugin storage', () => {
  let tmpDir: string;
  let pluginsDir: string;
  let dataDir: string;
  let uninstaller: PluginUninstaller;
  let plugins: Map<string, PluginInstance>;
  let storage: PluginStorageService;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owa-splitstore-'));
    // Split-dir deployment: the package dir and the ctx.storage data dir are NOT the same tree.
    pluginsDir = path.join(tmpDir, 'plugins');
    dataDir = path.join(tmpDir, 'data');
    fs.mkdirSync(pluginsDir, { recursive: true });
    const config = {
      get: (k: string) => (k === 'plugins.dir' ? pluginsDir : k === 'dataDir' ? dataDir : undefined),
    } as unknown as ConfigService;
    storage = new PluginStorageService(config);
    ({ uninstaller, plugins } = makeUninstaller(config, storage, pluginsDir));
  });
  afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));

  const writeUserPlugin = (id: string): string => {
    const dir = path.join(pluginsDir, id);
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(
      path.join(dir, 'manifest.json'),
      JSON.stringify({ id, name: id, version: '1.0.0', type: 'extension', main: 'index.js' }),
    );
    fs.writeFileSync(path.join(dir, 'index.js'), 'module.exports = class {};');
    return dir;
  };

  it('removes the plugin’s storage dir on uninstall, and only that plugin’s', async () => {
    const dir = writeUserPlugin('user-plg');
    writeUserPlugin('other-plg');
    plugins.set('user-plg', loadedUserPlugin('user-plg', dir));
    plugins.set('other-plg', loadedUserPlugin('other-plg', path.join(pluginsDir, 'other-plg')));
    seedEntry(storage, 'user-plg');
    seedEntry(storage, 'other-plg');
    // Persist real ctx.storage state for both plugins (split-dir: under <dataDir>/plugins/<id>).
    await storage.createPluginStorage('user-plg').set('token', { secret: 'abc' });
    await storage.createPluginStorage('other-plg').set('token', { secret: 'xyz' });
    const userStorageDir = path.join(dataDir, 'plugins', 'user-plg');
    const otherStorageDir = path.join(dataDir, 'plugins', 'other-plg');
    expect(fs.existsSync(userStorageDir)).toBe(true);

    await uninstaller.uninstallPlugin('user-plg');

    expect(fs.existsSync(dir)).toBe(false); // package dir (unchanged behavior)
    expect(fs.existsSync(userStorageDir)).toBe(false); // split-dir storage is now cleaned too
    expect(fs.existsSync(otherStorageDir)).toBe(true); // another plugin's storage is untouched
    expect(await storage.createPluginStorage('other-plg').get('token')).toEqual({ secret: 'xyz' });
  });
});
