import type { ThemeManager } from '@learnitive/color-scheme-manager';
import { commandHandlers as defaultCommandHandlers } from '@learnitive/command-handlers';
import { type LearnitiveAppCommand, learnitiveAppCommands } from '@learnitive/commands';
import {
  type EditorEngineId,
  THEME_MANAGER_CONFIG,
} from '@learnitive/constants';
import type { CoreServices } from '@learnitive/context';
import {
  type CoreConfigOverrides,
  createEditorSaveCoordinator,
  createServiceSetup,
} from '@learnitive/initialize-services/setup';
import { slot } from '@learnitive/poor-mans-di';
import { MemoryProjectStorage } from '@learnitive/project-storage';
import {
  FileStorageMemory,
  MemoryDatabaseService,
  MemoryRouterService,
  MemorySyncDatabaseService,
  TestErrorHandlerService,
} from '@learnitive/service-platform/testing';
import type {
  BaseServiceCommonOptions,
  CommandHandler,
  Store,
} from '@learnitive/types';
import { vi } from 'vitest';
import { type MockLog, makeTestCommonOpts } from './common-opts';

export type { Store } from '@learnitive/types';

export { default as waitForExpect } from 'wait-for-expect';

const themeManager = {
  currentPreference: THEME_MANAGER_CONFIG.defaultPreference,
  onThemeChange: () => () => {},
  setPreference: () => {},
  currentTheme: THEME_MANAGER_CONFIG.lightThemeClass,
} as unknown as ThemeManager;

const PLATFORM_SLOT_IDS = [
  'errorService',
  'database',
  'syncDatabase',
  'fileStorageMemory',
  'router',
] as const;

type TestServices = CoreServices & {
  errorService: TestErrorHandlerService;
  database: MemoryDatabaseService;
  syncDatabase: MemorySyncDatabaseService;
  fileStorageMemory: FileStorageMemory;
  router: MemoryRouterService;
};

type TestServiceSetup = {
  instantiate: (focus?: Array<keyof TestServices & string>) => TestServices;
  coreServices: () => CoreServices;
  mountAll: () => Promise<void>;
};

function createTestServiceSetup(
  commonOpts: BaseServiceCommonOptions,
  rootEmitter: ReturnType<typeof makeTestCommonOpts>['rootEmitter'],
  commands: LearnitiveAppCommand[],
  commandHandlers: Array<{ id: string; handler: CommandHandler }>,
  coreConfigOverrides: CoreConfigOverrides | undefined,
  editorEngineId: EditorEngineId,
): TestServiceSetup {
  const projectStorageProviders = new Map<string, MemoryProjectStorage>();
  return createServiceSetup({
    commonOpts,
    rootEmitter,
    commands,
    commandHandlers,
    themeManager,
    shortcutTarget: {
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
    },
    platformServices: {
      errorService: TestErrorHandlerService,
      database: MemoryDatabaseService,
      syncDatabase: MemorySyncDatabaseService,
      fileStorageMemory: slot(FileStorageMemory, () => ({
        onChange: (change) => {
          commonOpts.logger.info('File storage change:', change);
        },
      })),
      router: MemoryRouterService,
    },
    fileStorageSlots: ['fileStorageMemory'],
    createProjectStorageProvider: async (workspace) => {
      const existing = projectStorageProviders.get(workspace.name);
      if (existing) {
        return existing;
      }
      const storage = new MemoryProjectStorage();
      projectStorageProviders.set(workspace.name, storage);
      return storage;
    },
    editorEngineId,
    editorSaveCoordinator: createEditorSaveCoordinator(),
    coreConfigOverrides,
  });
}

export type TestEnvironment = {
  logger: ReturnType<typeof makeTestCommonOpts>['commonOpts']['logger'];
  mockLog: MockLog;
  controller: AbortController;
  rootEmitter: ReturnType<typeof makeTestCommonOpts>['rootEmitter'];
  commonOpts: ReturnType<typeof makeTestCommonOpts>['commonOpts'];
  store: Store;
  coreServices: () => CoreServices;
  mountAll: () => Promise<void>;
  instantiateAll: (focusService?: keyof TestServices & string) => TestServices;
};

/**
 * Creates a fully configured test environment with in-memory platform
 * services. Core wiring comes from the same `createServiceSetup` builder the
 * browser composition root uses, so test setup mirrors production setup.
 * Tests tune core services through `coreConfigOverrides` (decorators over the
 * canonical configs), or select the real editor implementation with
 * `editorEngineId`.
 */
export function createTestEnvironment({
  controller = new AbortController(),
  commands = learnitiveAppCommands,
  commandHandlers = defaultCommandHandlers,
  coreConfigOverrides,
  editorEngineId = 'prosemirror',
}: {
  controller?: AbortController;
  commands?: LearnitiveAppCommand[];
  commandHandlers?: Array<{ id: string; handler: CommandHandler }>;
  coreConfigOverrides?: CoreConfigOverrides;
  editorEngineId?: EditorEngineId;
} = {}): TestEnvironment {
  const { commonOpts, mockLog, rootEmitter } = makeTestCommonOpts({
    controller,
  });

  const setup = createTestServiceSetup(
    commonOpts,
    rootEmitter,
    commands,
    commandHandlers,
    coreConfigOverrides,
    editorEngineId,
  );

  return {
    logger: commonOpts.logger,
    mockLog,
    controller,
    rootEmitter,
    commonOpts,
    store: commonOpts.store as Store,

    /**
     * The typed core-facing service aggregate, as consumed by React.
     * Throws if called before `instantiateAll()`.
     */
    coreServices: setup.coreServices,

    /**
     * Mounts all services. Useful for ensuring all asynchronous initialization
     * completes before running tests.
     */
    mountAll: setup.mountAll,

    /**
     * Instantiates all services, optionally focusing on a particular service.
     * This is helpful if you want to bring up only a subset of services in tests.
     */
    instantiateAll: (focusService?: keyof TestServices & string) => {
      const focuses = focusService
        ? // always instantiate platform services plus the focused one
          [...PLATFORM_SLOT_IDS, focusService]
        : undefined;

      return setup.instantiate(focuses);
    },
  };
}
