import { describe, it, expect } from 'vitest';
import type { MessagingAdapter } from './adapter';
import type { MessageEvent } from './types';

export interface ConformanceOptions {
  /** Build a fresh, isolated adapter per test. */
  makeAdapter: () => Promise<MessagingAdapter> | MessagingAdapter;
  /** A thread id that exists in the fixture. */
  seededThreadId: string;
  /** Participant ids openThread can legally be called with. */
  openWith: string[];
}

/**
 * The definition of a correct MessagingAdapter. Every adapter runs this.
 *
 * Imports vitest, so it ships from the './conformance' subpath ONLY and is never
 * reachable from the main barrel. Consumers supply their own vitest.
 *
 * Usage from another package:
 *   import { runAdapterConformance } from '@insignia/iios-messaging-ui/conformance';
 *   runAdapterConformance({ makeAdapter: () => new MockAdapter(), seededThreadId: 'th_1', openWith: ['pp_a'] });
 */
export function runAdapterConformance(opts: ConformanceOptions): void {
  const make = async () => await opts.makeAdapter();

  describe('MessagingAdapter conformance', () => {
    it('lists conversations', async () => {
      const a = await make();
      const list = await a.listConversations();
      expect(Array.isArray(list)).toBe(true);
    });

    it('returns history for a seeded thread', async () => {
      const a = await make();
      const msgs = await a.history(opts.seededThreadId);
      expect(Array.isArray(msgs)).toBe(true);
    });

    it('send resolves with a message carrying the sent text and a stable id', async () => {
      const a = await make();
      const m = await a.send(opts.seededThreadId, 'conformance hello');
      expect(m.text).toBe('conformance hello');
      expect(typeof m.id).toBe('string');
      expect(m.id.length).toBeGreaterThan(0);
    });

    it('a sent message is attributed to the current actor', async () => {
      const a = await make();
      const m = await a.send(opts.seededThreadId, 'whose is this');
      expect(m.actorId).toBe(a.currentActorId());
    });

    it('currentActorId is known BEFORE any message is sent', async () => {
      // The bug this SDK exists to kill: identity must come from auth, never be
      // inferred from history. A fresh adapter already knows who you are.
      const a = await make();
      expect(a.currentActorId()).not.toBeNull();
    });

    it('a sent message appears in history', async () => {
      const a = await make();
      await a.send(opts.seededThreadId, 'persist me');
      const msgs = await a.history(opts.seededThreadId);
      expect(msgs.some((m) => m.text === 'persist me')).toBe(true);
    });

    it('subscribe delivers a message event on send', async () => {
      const a = await make();
      const seen: MessageEvent[] = [];
      const off = a.subscribe(opts.seededThreadId, (e) => seen.push(e));
      await a.send(opts.seededThreadId, 'live one');
      off();
      const msgs = seen.filter((e) => e.kind === 'message');
      expect(msgs.length).toBeGreaterThan(0);
    });

    it('unsubscribe stops delivery', async () => {
      const a = await make();
      const seen: MessageEvent[] = [];
      const off = a.subscribe(opts.seededThreadId, (e) => seen.push(e));
      off();
      await a.send(opts.seededThreadId, 'should not be heard');
      expect(seen).toHaveLength(0);
    });

    it('unsubscribe is idempotent', async () => {
      const a = await make();
      const off = a.subscribe(opts.seededThreadId, () => {});
      off();
      expect(() => off()).not.toThrow();
    });

    it('openThread returns a thread id', async () => {
      const a = await make();
      const { threadId } = await a.openThread({ participantIds: opts.openWith });
      expect(typeof threadId).toBe('string');
      expect(threadId.length).toBeGreaterThan(0);
    });

    it('markRead resolves', async () => {
      const a = await make();
      const m = await a.send(opts.seededThreadId, 'read me');
      await expect(a.markRead(opts.seededThreadId, m.id)).resolves.toBeUndefined();
    });

    it('sendTyping does not throw', async () => {
      const a = await make();
      expect(() => a.sendTyping(opts.seededThreadId)).not.toThrow();
    });
  });
}
