import { Injector } from '@angular/core';
import { provideTranslateService } from '@ngx-translate/core';
import { environment } from '../../../../../environments/environment';
import { AppI18nService } from '../../../../core/i18n';
import type { TojuPluginManifest } from '../../../../shared-kernel';
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import { PluginStoreService } from './plugin-store.service';
import { PluginHostService } from './plugin-host.service';
import { PluginDesktopStateService } from './plugin-desktop-state.service';
import { PluginRequirementService } from './plugin-requirement.service';
import { PluginRegistryService } from './plugin-registry.service';
import { PluginCapabilityService } from './plugin-capability.service';
import type { PluginStoreEntry } from '../../domain/models/plugin-store.models';

const OFFICIAL_PLUGIN_SOURCE_URL = environment.pluginStore.defaultSourceUrls[0];

describe('PluginStoreService', () => {
  let fetchMock: ReturnType<typeof vi.fn>;
  let registerLocalManifest: ReturnType<typeof vi.fn>;
  let unregister: ReturnType<typeof vi.fn>;
  let storage: Storage;

  beforeEach(() => {
    storage = createMemoryStorage();
    vi.stubGlobal('localStorage', storage);
    fetchMock = vi.fn();
    registerLocalManifest = vi.fn((manifest: TojuPluginManifest, sourcePath?: string) => ({
      enabled: true,
      manifest,
      sourcePath,
      state: 'validated',
      validationIssues: []
    }));

    unregister = vi.fn();
    vi.stubGlobal('fetch', fetchMock);
  });

  afterEach(() => {
    storage.clear();
    vi.unstubAllGlobals();
  });

  it('loads plugin entries from source manifests and resolves relative links', async () => {
    mockFetchResponses(fetchMock, {
      'https://plugins.example.test/index.json': jsonResponse({
        plugins: [
          {
            author: 'Ada Example',
            description: 'Adds better channel tools.',
            github: 'https://github.com/example/better-channels',
            id: 'example.better-channels',
            image: './images/better.png',
            install: './better/toju-plugin.json',
            readme: './better/README.md',
            title: 'Better Channels',
            version: '1.2.0'
          }
        ],
        title: 'Example Plugins'
      })
    });

    const service = createService(registerLocalManifest, unregister);

    await new Promise((resolve) => setTimeout(resolve, 0));
    await service.addSourceUrl('https://plugins.example.test/index.json#latest');

    expect(service.sourceUrls()).toEqual([OFFICIAL_PLUGIN_SOURCE_URL, 'https://plugins.example.test/index.json']);

    expect(service.sources().some((source) => source.title === 'Example Plugins')).toBe(true);
    expect(service.availablePlugins()).toContainEqual(expect.objectContaining({
      author: 'Ada Example',
      githubUrl: 'https://github.com/example/better-channels',
      id: 'example.better-channels',
      imageUrl: 'https://plugins.example.test/images/better.png',
      installUrl: 'https://plugins.example.test/better/toju-plugin.json',
      readmeUrl: 'https://plugins.example.test/better/README.md',
      sourceTitle: 'Example Plugins',
      title: 'Better Channels',
      version: '1.2.0'
    }));
  });

  it('seeds the official plugin repository for new users', async () => {
    const service = createService(registerLocalManifest, unregister);

    await new Promise((resolve) => setTimeout(resolve, 0));

    expect(service.sourceUrls()).toEqual([OFFICIAL_PLUGIN_SOURCE_URL]);
    expect(fetchMock).toHaveBeenCalledWith(
      OFFICIAL_PLUGIN_SOURCE_URL,
      expect.objectContaining({
        headers: { Accept: 'application/json' }
      })
    );
  });

  it('adds the official plugin repository when loading legacy source lists', () => {
    storage.setItem('metoyou_plugin_store', JSON.stringify({
      installedPlugins: [],
      schemaVersion: 1,
      sourceUrls: ['https://plugins.example.test/index.json']
    }));

    const service = createService(registerLocalManifest, unregister);

    expect(service.sourceUrls()).toEqual([OFFICIAL_PLUGIN_SOURCE_URL, 'https://plugins.example.test/index.json']);
  });

  it('keeps user-removed default sources removed after schema migration', () => {
    storage.setItem('metoyou_plugin_store', JSON.stringify({
      installedPlugins: [],
      schemaVersion: 2,
      sourceUrls: ['https://plugins.example.test/index.json']
    }));

    const service = createService(registerLocalManifest, unregister);

    expect(service.sourceUrls()).toEqual(['https://plugins.example.test/index.json']);
  });

  it('accepts local source manifest paths and resolves relative file links', async () => {
    const localSourceManifest = {
      plugins: [
        {
          description: 'Local plugin source.',
          id: 'example.local-plugin',
          image: './icon.svg',
          install: './toju-plugin.json',
          readme: './README.md',
          title: 'Local Plugin',
          version: '1.0.0'
        }
      ],
      title: 'Local Plugins'
    };
    const grantPluginReadRoot = vi.fn(async () => true);
    const readFile = vi.fn(async () => toBase64(JSON.stringify(localSourceManifest)));
    const service = createService(registerLocalManifest, unregister, { grantPluginReadRoot, readFile });

    await new Promise((resolve) => setTimeout(resolve, 0));
    await service.addSourceUrl('/home/ludde/Desktop/TestPlugin/plugin-source.json');

    expect(fetchMock).not.toHaveBeenCalledWith('/home/ludde/Desktop/TestPlugin/plugin-source.json', expect.anything());
    expect(grantPluginReadRoot).toHaveBeenCalledWith('/home/ludde/Desktop/TestPlugin');
    expect(readFile).toHaveBeenCalledWith('/home/ludde/Desktop/TestPlugin/plugin-source.json');
    expect(service.sourceUrls()).toEqual([OFFICIAL_PLUGIN_SOURCE_URL, 'file:///home/ludde/Desktop/TestPlugin/plugin-source.json']);

    expect(service.availablePlugins()).toEqual([
      expect.objectContaining({
        id: 'example.local-plugin',
        imageUrl: 'file:///home/ludde/Desktop/TestPlugin/icon.svg',
        installUrl: 'file:///home/ludde/Desktop/TestPlugin/toju-plugin.json',
        readmeUrl: 'file:///home/ludde/Desktop/TestPlugin/README.md',
        sourceTitle: 'Local Plugins'
      })
    ]);
  });

  it('installs, detects updates, and uninstalls store plugins', async () => {
    const manifest = createManifest({ version: '1.0.0' });
    const plugin = createStoreEntry({ version: '1.0.0' });

    mockFetchResponses(fetchMock, {
      [plugin.installUrl ?? '']: jsonResponse(manifest)
    });

    const service = createService(registerLocalManifest, unregister);

    await service.installPlugin(plugin);

    expect(registerLocalManifest).toHaveBeenCalledWith(manifest, plugin.installUrl);
    expect(service.installedPlugins()[0]?.manifest.id).toBe(plugin.id);
    expect(service.getActionLabel(plugin)).toBe('Uninstall');
    expect(service.getActionLabel(createStoreEntry({ version: '1.1.0' }))).toBe('Update');

    await service.uninstallPlugin(plugin.id);

    expect(unregister).toHaveBeenCalledWith(plugin.id);
    expect(service.installedPlugins()).toEqual([]);
  });

  it('caches plugin bundle entrypoints locally before registering installed plugins', async () => {
    const manifest = createManifest({ entrypoint: './dist/main.js' });
    const plugin = createStoreEntry({
      bundleUrl: 'https://plugins.example.test/better/bundle.js',
      version: '1.0.0'
    });
    const electronApi = {
      ensureDir: vi.fn(async () => true),
      getAppDataPath: vi.fn(async () => '/tmp/metoyou-user-data'),
      writeFile: vi.fn(async () => true)
    };

    mockFetchResponses(fetchMock, {
      [plugin.bundleUrl ?? '']: textResponse('export function activate() {}'),
      [plugin.installUrl ?? '']: jsonResponse(manifest)
    });

    const service = createService(registerLocalManifest, unregister, electronApi);

    await service.installPlugin(plugin);

    expect(electronApi.ensureDir).toHaveBeenCalledWith('/tmp/metoyou-user-data/plugin-bundles/example.better-channels/1.0.0');
    expect(electronApi.writeFile).toHaveBeenCalledWith(
      '/tmp/metoyou-user-data/plugin-bundles/example.better-channels/1.0.0/main.js',
      expect.any(String)
    );

    expect(registerLocalManifest).toHaveBeenCalledWith(
      expect.objectContaining({
        bundle: {
          entrypoint: './main.js',
          url: plugin.bundleUrl
        },
        entrypoint: './main.js',
        id: manifest.id
      }),
      'file:///tmp/metoyou-user-data/plugin-bundles/example.better-channels/1.0.0/toju-plugin.json'
    );
  });

  it('loads plugin readmes as markdown text', async () => {
    const plugin = createStoreEntry({ readmeUrl: 'https://plugins.example.test/better/README.md' });

    mockFetchResponses(fetchMock, {
      [plugin.readmeUrl ?? '']: textResponse('# Better Channels')
    });

    const service = createService(registerLocalManifest, unregister);
    const readme = await service.loadReadme(plugin);

    expect(readme).toEqual({
      markdown: '# Better Channels',
      pluginId: plugin.id,
      title: plugin.title,
      url: plugin.readmeUrl
    });
  });

  it('allows localhost HTTP plugin source URLs for local dev and E2E', async () => {
    const localSourceUrl = 'http://localhost:4200/plugins/e2e-plugin-source.json';

    mockFetchResponses(fetchMock, {
      [localSourceUrl]: jsonResponse({
        title: 'Local E2E Source',
        plugins: []
      })
    });

    const service = createService(registerLocalManifest, unregister);

    await expect(service.addSourceUrl(localSourceUrl)).resolves.toBeUndefined();
    expect(service.sourceUrls()).toContain(localSourceUrl);
  });
});

function mockFetchResponses(fetchMock: ReturnType<typeof vi.fn>, responses: Record<string, Response>): void {
  fetchMock.mockImplementation(async (url: RequestInfo | URL) => {
    const requestUrl = typeof url === 'string'
      ? url
      : url instanceof URL
        ? url.toString()
        : url.url;

    return responses[requestUrl]
      ?? (requestUrl === OFFICIAL_PLUGIN_SOURCE_URL
        ? jsonResponse({ plugins: [], title: 'Official Toju Plugins' })
        : textResponse(''));
  });
}

function createService(
  registerLocalManifest: ReturnType<typeof vi.fn>,
  unregister: ReturnType<typeof vi.fn>,
  electronApi: {
    ensureDir?: (dirPath: string) => Promise<boolean>;
    getAppDataPath?: () => Promise<string>;
    grantPluginReadRoot?: (rootPath: string) => Promise<boolean>;
    readFile?: (filePath: string) => Promise<string | null>;
    writeFile?: (filePath: string, data: string) => Promise<boolean>;
  } | null = null
): PluginStoreService {
  const injector = Injector.create({
    providers: [
      provideTranslateService({
        fallbackLang: 'en',
        lang: 'en'
      }),
      AppI18nService,
      PluginStoreService,
      {
        provide: ElectronBridgeService,
        useValue: {
          getApi: vi.fn(() => electronApi)
        }
      },
      {
        provide: PluginHostService,
        useValue: {
          activatePersistedPlugins: vi.fn(async () => {}),
          deactivatePlugin: vi.fn(async () => {}),
          isPluginActive: vi.fn(() => false),
          registerLocalManifest
        }
      },
      {
        provide: PluginDesktopStateService,
        useValue: {
          readJson: vi.fn(async (_key: string, fallback: unknown) => fallback),
          writeJson: vi.fn(async () => undefined)
        }
      },
      {
        provide: PluginCapabilityService,
        useValue: {
          grantAll: vi.fn()
        }
      },
      {
        provide: PluginRegistryService,
        useValue: { unregister }
      },
      {
        provide: PluginRequirementService,
        useValue: {}
      }
    ]
  });
  const service = injector.get(PluginStoreService);

  injector.get(AppI18nService).initialize();

  return service;
}

function toBase64(value: string): string {
  return Buffer.from(value, 'utf8').toString('base64');
}

function createManifest(overrides: Partial<TojuPluginManifest> = {}): TojuPluginManifest {
  return {
    apiVersion: '1.0.0',
    compatibility: {
      minimumTojuVersion: '1.0.0'
    },
    description: 'Adds better channel tools.',
    entrypoint: './dist/main.js',
    id: 'example.better-channels',
    kind: 'client',
    schemaVersion: 1,
    title: 'Better Channels',
    version: '1.0.0',
    ...overrides
  };
}

function createStoreEntry(overrides: Partial<PluginStoreEntry> = {}): PluginStoreEntry {
  return {
    author: 'Ada Example',
    description: 'Adds better channel tools.',
    githubUrl: 'https://github.com/example/better-channels',
    id: 'example.better-channels',
    imageUrl: 'https://plugins.example.test/images/better.png',
    installUrl: 'https://plugins.example.test/better/toju-plugin.json',
    readmeUrl: 'https://plugins.example.test/better/README.md',
    sourceTitle: 'Example Plugins',
    sourceUrl: 'https://plugins.example.test/index.json',
    title: 'Better Channels',
    version: '1.0.0',
    ...overrides
  };
}

function jsonResponse(value: unknown): Response {
  return {
    json: vi.fn(async () => value),
    ok: true,
    status: 200,
    text: vi.fn(async () => JSON.stringify(value))
  } as unknown as Response;
}

function textResponse(value: string): Response {
  return {
    json: vi.fn(async () => JSON.parse(value) as unknown),
    ok: true,
    status: 200,
    text: vi.fn(async () => value)
  } as unknown as Response;
}

function createMemoryStorage(): Storage {
  const values = new Map<string, string>();

  return {
    get length(): number {
      return values.size;
    },
    clear: vi.fn(() => values.clear()),
    getItem: vi.fn((key: string) => values.get(key) ?? null),
    key: vi.fn((index: number) => Array.from(values.keys())[index] ?? null),
    removeItem: vi.fn((key: string) => values.delete(key)),
    setItem: vi.fn((key: string, value: string) => values.set(key, value))
  };
}
