import { Injector, runInInjectionContext } from '@angular/core';
import { Subject } from 'rxjs';
import { DatabaseService } from '../../../infrastructure/persistence';
import { RealtimeSessionFacade } from '../../../core/realtime';
import { CustomEmoji } from '../../../shared-kernel';
import { CustomEmojiService } from './custom-emoji.service';

function customEmoji(overrides: Partial<CustomEmoji> = {}): CustomEmoji {
  const dataUrl = overrides.dataUrl ?? 'data:image/webp;base64,YWJjZA==';

  return {
    id: 'emoji-1',
    name: 'party',
    creatorUserId: 'user-1',
    dataUrl,
    hash: overrides.hash ?? '',
    mime: 'image/webp',
    size: 4,
    createdAt: 100,
    updatedAt: 100,
    ...overrides
  };
}

async function hashText(value: string): Promise<string> {
  const bytes = new TextEncoder().encode(value);
  const digest = await crypto.subtle.digest('SHA-256', bytes);

  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
}

describe('CustomEmojiService', () => {
  let db: Pick<DatabaseService, 'getCustomEmojis' | 'saveCustomEmoji' | 'deleteCustomEmoji'>;
  let webrtc: Pick<RealtimeSessionFacade, 'getConnectedPeers' | 'sendToPeer' | 'sendToPeerBuffered' | 'onPeerConnected' | 'onMessageReceived'>;

  beforeEach(() => {
    db = {
      getCustomEmojis: vi.fn(async () => []),
      saveCustomEmoji: vi.fn(async () => undefined),
      deleteCustomEmoji: vi.fn(async () => undefined)
    };

    webrtc = {
      getConnectedPeers: vi.fn(() => []),
      sendToPeer: vi.fn(),
      sendToPeerBuffered: vi.fn(async () => undefined),
      onPeerConnected: new Subject<string>(),
      onMessageReceived: new Subject()
    };
  });

  function createService(): CustomEmojiService {
    const injector = Injector.create({
      providers: [
        CustomEmojiService,
        { provide: DatabaseService, useValue: db },
        { provide: RealtimeSessionFacade, useValue: webrtc }
      ]
    });

    return runInInjectionContext(injector, () => injector.get(CustomEmojiService));
  }

  it('does not persist a stale remote emoji over a newer local emoji', async () => {
    const dataUrl = 'data:image/webp;base64,YWJjZA==';
    const hash = await hashText(dataUrl);
    const local = customEmoji({ dataUrl,
      hash,
      updatedAt: 200 });
    const staleRemote = customEmoji({ dataUrl,
      hash,
      updatedAt: 100 });

    vi.mocked(db.getCustomEmojis).mockResolvedValueOnce([local]);
    const service = createService();

    await service.loadForUser('user-1');

    await service.saveRemoteEmoji(staleRemote);

    expect(db.saveCustomEmoji).not.toHaveBeenCalled();
    expect(service.findEmoji('emoji-1')).toBe(local);
  });

  it('does not persist a completed transfer when its data does not match the manifest', async () => {
    const service = createService();
    const goodDataUrl = 'data:image/webp;base64,QUJDRA==';
    const manifest = customEmoji({
      dataUrl: goodDataUrl,
      hash: await hashText(goodDataUrl),
      size: 4
    });
    const { dataUrl, ...transferManifest } = manifest;

    service.receiveTransferStart(transferManifest, 1);
    await service.receiveTransferChunk(manifest.id, 0, 1, 'QUJD');

    expect(db.saveCustomEmoji).not.toHaveBeenCalled();
    expect(service.findEmoji(manifest.id)).toBeUndefined();
  });

  it('drops invalid locally stored emoji before advertising them to peers', async () => {
    const service = createService();
    const corruptEmoji = customEmoji({
      dataUrl: 'data:image/webp;base64,QUJD',
      hash: await hashText('data:image/webp;base64,QUJDRA=='),
      size: 4
    });

    vi.mocked(db.getCustomEmojis).mockResolvedValueOnce([corruptEmoji]);

    await service.loadForUser('user-1');

    expect(service.findEmoji(corruptEmoji.id)).toBeUndefined();
    expect(db.deleteCustomEmoji).toHaveBeenCalledWith(corruptEmoji.id);
    expect(service.buildSummary()).toEqual([]);
  });

  it('removes a saved emoji from the library while keeping it available for rendering', async () => {
    const service = createService();
    const dataUrl = 'data:image/webp;base64,QUJDRA==';
    const savedEmoji = customEmoji({
      dataUrl,
      hash: await hashText(dataUrl),
      size: 4,
      savedByUser: true
    });

    await service.saveRemoteEmoji(savedEmoji);
    expect(service.isEmojiInLibrary(savedEmoji.id)).toBe(true);

    await service.removeEmojiFromLibrary(savedEmoji.id);

    expect(service.isEmojiInLibrary(savedEmoji.id)).toBe(false);
    expect(service.findEmoji(savedEmoji.id)).toEqual(expect.objectContaining({ id: savedEmoji.id }));
    expect(service.emojis()).toEqual([]);
    expect(db.saveCustomEmoji).toHaveBeenLastCalledWith(expect.objectContaining({
      id: savedEmoji.id,
      savedByUser: false
    }));
  });

  it('keeps synced remote emoji out of the library until the user saves them', async () => {
    const service = createService();
    const dataUrl = 'data:image/webp;base64,QUJDRA==';
    const remoteEmoji = customEmoji({
      dataUrl,
      hash: await hashText(dataUrl),
      size: 4,
      updatedAt: 200
    });

    await service.saveRemoteEmoji(remoteEmoji);

    expect(service.findEmoji(remoteEmoji.id)).toEqual(expect.objectContaining({ id: remoteEmoji.id }));
    expect(service.emojis()).toEqual([]);

    await service.saveEmojiToLibrary(remoteEmoji.id);

    expect(service.emojis()).toEqual([
      expect.objectContaining({ id: remoteEmoji.id,
        savedByUser: true })
    ]);
  });

  it('pushes referenced custom emoji assets to every connected peer without waiting for a request', async () => {
    const service = createService();
    const dataUrl = 'data:image/webp;base64,QUJDRA==';
    const emoji = customEmoji({
      id: 'party-id',
      dataUrl,
      hash: await hashText(dataUrl),
      size: 4,
      savedByUser: true
    });

    await service.saveRemoteEmoji(emoji);
    vi.mocked(webrtc.getConnectedPeers).mockReturnValue(['peer-a', 'peer-b']);

    const sent: { peerId: string; event: { type: string } }[] = [];

    vi.mocked(webrtc.sendToPeerBuffered).mockImplementation(async (peerId, event) => {
      sent.push({ peerId,
        event: event as { type: string } });
    });

    await service.pushEmojisForContent('Hello :emoji[party-id](party)');

    expect(sent.map((entry) => entry.peerId).sort()).toEqual(['peer-a', 'peer-b']);
    expect(sent.every((entry) => entry.event.type === 'custom-emoji-full')).toBe(true);
    expect(sent.every((entry) => 'customEmoji' in entry.event)).toBe(true);
    expect(sent.some((entry) => entry.event.type === 'custom-emoji-chunk')).toBe(false);
  });

  it('requests missing custom emoji assets referenced by incoming messages', async () => {
    const service = createService();

    await service.requestMissingFromMessageContent('peer-1', 'Take this :emoji[download](download)');

    expect(webrtc.sendToPeer).toHaveBeenCalledWith('peer-1', {
      type: 'custom-emoji-request',
      ids: ['download']
    });
  });

  it('round-trips a multi-chunk emoji from sender to receiver so it renders', async () => {
    const sender = createService();
    const bytes = new Uint8Array(40 * 1024);

    for (let index = 0; index < bytes.length; index++) {
      bytes[index] = index % 251;
    }

    const base64 = Buffer.from(bytes).toString('base64');
    const dataUrl = `data:image/webp;base64,${base64}`;
    const created = customEmoji({
      id: 'download',
      name: 'download',
      dataUrl,
      hash: await hashText(dataUrl),
      mime: 'image/webp',
      size: bytes.length,
      savedByUser: true
    });

    await sender.loadForUser('creator-1');
    await sender.saveRemoteEmoji(created);
    expect(sender.findEmoji(created.id)).toBeDefined();

    const sent: ({ type: string } & Record<string, unknown>)[] = [];

    vi.mocked(webrtc.sendToPeerBuffered).mockImplementation(async (_peerId, event) => {
      sent.push(event as { type: string } & Record<string, unknown>);
    });

    await sender.sendRequestedToPeer('receiver-peer', [created.id]);

    const start = sent.find((event) => event.type === 'custom-emoji-full');
    const chunks = sent.filter((event) => event.type === 'custom-emoji-chunk');

    expect(start).toBeDefined();
    expect(chunks.length).toBeGreaterThan(1);

    if (!start || typeof start.total !== 'number') {
      throw new Error('Expected custom emoji transfer start event.');
    }

    const receiver = createService();

    receiver.receiveTransferStart(
      start.customEmojiTransfer as never,
      start.total
    );

    vi.mocked(db.saveCustomEmoji).mockClear();

    for (const chunk of chunks) {
      await receiver.receiveTransferChunk(
        chunk['customEmojiId'] as string,
        chunk['index'] as number,
        chunk['total'] as number,
        chunk['data'] as string
      );
    }

    const received = receiver.findEmoji(created.id);

    expect(received).toBeDefined();
    expect(received?.dataUrl).toBe(created.dataUrl);
  });
});
