import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest';
import { createPluginManager } from './plugin-manager';
import { loadManifestCode, getValidUrl, prepareUrl } from './parse-manifest.js';
import { PluginModalElement } from './modal/plugin-modal.js';
import { openUIApi } from './api/openUI.api.js';
import type { Context, Theme } from '@penpot/plugin-types';
import type { Manifest } from './models/manifest.model.js';

vi.mock('./parse-manifest.js', () => ({
  loadManifestCode: vi.fn(),
  getValidUrl: vi.fn(),
  prepareUrl: vi.fn(),
}));

vi.mock('./api/openUI.api.js', () => ({
  openUIApi: vi.fn(),
}));

describe('createPluginManager', () => {
  let mockContext: Context;
  let manifest: Manifest;
  let onCloseCallback: () => void;
  let onReloadModal: () => void;
  let mockModal: {
    setTheme: ReturnType<typeof vi.fn>;
    remove: ReturnType<typeof vi.fn>;
    addEventListener: ReturnType<typeof vi.fn>;
    removeEventListener: ReturnType<typeof vi.fn>;
    getAttribute: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    manifest = {
      pluginId: 'test-plugin',
      name: 'Test Plugin',
      host: 'https://example.com',
      code: '',
      permissions: [
        'content:read',
        'content:write',
        'library:read',
        'library:write',
        'user:read',
        'comment:read',
        'comment:write',
        'allow:downloads',
        'allow:localstorage',
      ],
    };

    mockModal = {
      setTheme: vi.fn(),
      remove: vi.fn(),
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      getAttribute: vi.fn(),
    };

    vi.mocked(openUIApi).mockReturnValue(
      mockModal as unknown as PluginModalElement,
    );

    mockContext = {
      theme: 'light',
      addListener: vi.fn().mockReturnValue(Symbol()),
      removeListener: vi.fn(),
    } as unknown as Context;

    onCloseCallback = vi.fn();
    onReloadModal = vi.fn();

    vi.mocked(loadManifestCode).mockResolvedValue(
      'console.log("Plugin loaded");',
    );
    vi.mocked(getValidUrl).mockReturnValue(
      new URL('https://example.com/plugin'),
    );
    vi.mocked(prepareUrl).mockReturnValue('https://example.com/plugin');
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  it('should load the plugin and set up listeners', async () => {
    await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    expect(loadManifestCode).toHaveBeenCalledWith(manifest);
    expect(mockContext.addListener).toHaveBeenCalledWith(
      'themechange',
      expect.any(Function),
    );
    expect(mockContext.addListener).toHaveBeenCalledWith(
      'finish',
      expect.any(Function),
    );
  });

  it('should open a modal with the correct URL and theme', async () => {
    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    pluginManager.openModal('Test Modal', '/test-url', {
      width: 400,
      height: 300,
    });

    expect(prepareUrl).toHaveBeenCalledWith(manifest, '/test-url', {
      theme: 'light',
    });
    expect(openUIApi).toHaveBeenCalledWith(
      'Test Modal',
      'https://example.com/plugin',
      'light',
      { width: 400, height: 300 },
      true,
      false,
      false,
    );
    expect(mockModal.setTheme).toHaveBeenCalledWith('light');
    expect(mockModal.addEventListener).toHaveBeenCalledWith(
      'close',
      expect.any(Function),
      { once: true },
    );
    expect(mockModal.addEventListener).toHaveBeenCalledWith(
      'load',
      expect.any(Function),
    );
  });

  it('should not open a new modal if the URL has not changed', async () => {
    mockModal.getAttribute.mockReturnValue('https://example.com/plugin');

    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    pluginManager.openModal('Test Modal', '/test-url');
    pluginManager.openModal('Test Modal', '/test-url');

    expect(openUIApi).toHaveBeenCalledTimes(1);
  });

  it('should handle theme changes and update the modal theme', async () => {
    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    pluginManager.openModal('Test Modal', '/test-url');

    const themeChangeCallback = vi
      .mocked(mockContext.addListener)
      .mock.calls.find((call) => call[0] === 'themechange')?.[1];

    if (!themeChangeCallback) {
      throw new Error('Theme change callback not found');
    }

    themeChangeCallback('dark' as Theme);

    expect(mockModal.setTheme).toHaveBeenCalledWith('dark');
  });

  it('should remove all event listeners and close the plugin', async () => {
    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    pluginManager.openModal('Test Modal', '/test-url');

    pluginManager.close();

    expect(mockContext.removeListener).toHaveBeenCalled();
    expect(mockModal.removeEventListener).toHaveBeenCalledWith(
      'close',
      expect.any(Function),
    );
    expect(mockModal.remove).toHaveBeenCalled();
    expect(onCloseCallback).toHaveBeenCalled();
  });

  it('shoud clean setTimeout when plugin is closed', async () => {
    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    pluginManager.timeouts.add(setTimeout(() => {}, 1000));
    pluginManager.timeouts.add(setTimeout(() => {}, 1000));

    expect(pluginManager.timeouts.size).toBe(2);

    pluginManager.close();

    expect(pluginManager.timeouts.size).toBe(0);
  });

  it('should reload the modal when reloaded', async () => {
    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    await pluginManager.openModal('Test Modal', '/test-url');

    const loadCallback = mockModal.addEventListener.mock.calls.find((call) => {
      return call[0] === 'load';
    });

    if (loadCallback) {
      // initial load
      await loadCallback[1]();

      // reload
      await loadCallback[1]();

      expect(onReloadModal).toHaveBeenCalledWith(
        'console.log("Plugin loaded");',
      );
    }
  });

  it('should register and trigger message callbacks', async () => {
    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    const callback = vi.fn();
    pluginManager.registerMessageCallback(callback);

    pluginManager.sendMessage('Test Message');

    expect(callback).toHaveBeenCalledWith('Test Message');
  });

  it('should register and remove listeners', async () => {
    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    const callback = vi.fn();
    const listenerId = pluginManager.registerListener('themechange', callback);

    expect(mockContext.addListener).toHaveBeenCalledWith(
      'themechange',
      expect.any(Function),
      undefined,
    );

    pluginManager.destroyListener(listenerId);

    expect(mockContext.removeListener).toHaveBeenCalledWith(listenerId);
  });

  it('should clean up all event listeners on close', async () => {
    const pluginManager = await createPluginManager(
      mockContext,
      manifest,
      onCloseCallback,
      onReloadModal,
    );

    pluginManager.close();

    expect(mockContext.removeListener).toHaveBeenCalled();
    expect(onCloseCallback).toHaveBeenCalled();
  });
});
