/**
 * V3.11 acceptance criterion: P2P-død → HTTP innen 5 s uten meldingstap.
 *
 * We simulate WebRTC failure by injecting a factory whose every peer
 * connection refuses to open. The MultiTransportFallback should
 * demote to HTTP, and the upload should complete via the HTTP
 * receiver-side route exactly as if WebRTC was never configured.
 */
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import {
  createShade,
  type Shade,
  type TransferHandle,
  type TransferResult,
} from '../src/index.js';
import {
  createPrekeyServer,
  MemoryPrekeyStore,
  PrekeyServerEvents,
} from '@shade/server';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { sha256Once } from '@shade/streams';
import type {
  IDataChannel,
  IPeerConnection,
  IRtcFactory,
  ShadeIceCandidate,
  ShadeRtcConfig,
  ShadeRtcConnectionState,
  ShadeSessionDescription,
} from '@shade/transport-webrtc';

const crypto = new SubtleCryptoProvider();

/** Factory whose PCs synthesize an SDP but never emit `'open'` on the data
 *  channel. Triggers a connect timeout, which the multi-fallback treats
 *  as a transport error and demotes the WebRTC layer. */
class BrokenRtcFactory implements IRtcFactory {
  createPeerConnection(_config: ShadeRtcConfig): IPeerConnection {
    return new BrokenPeerConnection();
  }
}

class BrokenPeerConnection implements IPeerConnection {
  connectionState: ShadeRtcConnectionState | string = 'new';
  iceConnectionState = 'new';
  private dc: BrokenDataChannel | null = null;

  createDataChannel(label: string): IDataChannel {
    if (this.dc !== null) return this.dc;
    this.dc = new BrokenDataChannel(label);
    return this.dc;
  }
  async createOffer(): Promise<ShadeSessionDescription> {
    return { type: 'offer', sdp: 'v=0\nbroken' };
  }
  async createAnswer(): Promise<ShadeSessionDescription> {
    return { type: 'answer', sdp: 'v=0\nbroken' };
  }
  async setLocalDescription(_desc: ShadeSessionDescription): Promise<void> {}
  async setRemoteDescription(_desc: ShadeSessionDescription): Promise<void> {}
  async addIceCandidate(_c: ShadeIceCandidate | null): Promise<void> {}
  close(): void {
    this.connectionState = 'closed';
    if (this.dc !== null) this.dc.close();
  }
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  addEventListener(_event: string, _cb: any): void {
    // Never fires open / datachannel — provoking the connect timeout.
  }
  removeEventListener(): void {}
}

class BrokenDataChannel implements IDataChannel {
  readyState: 'connecting' | 'open' | 'closing' | 'closed' = 'connecting';
  binaryType: 'arraybuffer' | 'blob' = 'arraybuffer';
  bufferedAmount = 0;
  constructor(public readonly label: string) {}
  send(_data: ArrayBuffer | Uint8Array): void {
    throw new Error('broken DC');
  }
  close(): void {
    this.readyState = 'closed';
  }
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  addEventListener(_event: string, _cb: any): void {}
  removeEventListener(): void {}
}

interface Rig {
  alice: Shade;
  bob: Shade;
  prekeyStop: () => void;
  aliceServerStop: () => void;
  bobServerStop: () => void;
}

async function startPrekeyServer(): Promise<{ url: string; stop: () => void }> {
  const events = new PrekeyServerEvents();
  const server = createPrekeyServer({
    crypto,
    store: new MemoryPrekeyStore(),
    disableRateLimit: true,
    events,
  });
  const port = 23000 + Math.floor(Math.random() * 500);
  const handle = Bun.serve({ port, fetch: server.fetch });
  return { url: `http://localhost:${port}`, stop: () => handle.stop() };
}

async function setupRig(connectTimeoutMs: number): Promise<Rig> {
  const prekey = await startPrekeyServer();
  const alice = await createShade({ prekeyServer: prekey.url, address: 'alice' });
  const bob = await createShade({ prekeyServer: prekey.url, address: 'bob' });

  const baseUrls = new Map<string, string>();
  const resolveBaseUrl = async (addr: string): Promise<string> => {
    const url = baseUrls.get(addr);
    if (url === undefined) throw new Error(`unknown peer ${addr}`);
    return url;
  };
  alice.configureTransfers({ resolveBaseUrl });
  bob.configureTransfers({ resolveBaseUrl });

  const broken = new BrokenRtcFactory();
  alice.configureWebRTC({ factory: broken, connectTimeoutMs });
  bob.configureWebRTC({ factory: broken, connectTimeoutMs });

  const bobApp = await bob.transferRoute();
  const bobPort = 23500 + Math.floor(Math.random() * 500);
  const bobServer = Bun.serve({ port: bobPort, fetch: bobApp.fetch });
  baseUrls.set('bob', `http://localhost:${bobPort}`);

  const aliceApp = await alice.transferRoute();
  const alicePort = 24000 + Math.floor(Math.random() * 500);
  const aliceServer = Bun.serve({ port: alicePort, fetch: aliceApp.fetch });
  baseUrls.set('alice', `http://localhost:${alicePort}`);

  return {
    alice,
    bob,
    prekeyStop: prekey.stop,
    aliceServerStop: () => aliceServer.stop(),
    bobServerStop: () => bobServer.stop(),
  };
}

async function teardownRig(rig: Rig): Promise<void> {
  await rig.alice.shutdown();
  await rig.bob.shutdown();
  rig.bobServerStop();
  rig.aliceServerStop();
  rig.prekeyStop();
}

function hex(b: Uint8Array): string {
  return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
}

describe('V3.11 P2P → HTTP failover', () => {
  let rig: Rig;
  beforeAll(async () => {
    // 2s timeout — well within the 5s acceptance budget.
    rig = await setupRig(2_000);
  });
  afterAll(async () => {
    await teardownRig(rig);
  });

  test(
    'WebRTC primary fails → HTTP fallback delivers without message loss',
    async () => {
      const input = crypto.randomBytes(64 * 1024);

      let resolveRecv!: (h: TransferHandle) => void;
      const recvHandlePromise = new Promise<TransferHandle>((r) => {
        resolveRecv = r;
      });
      const unsubscribe = await rig.bob.onIncomingTransfer(async (incoming) => {
        const h = await incoming.accept({ output: { kind: 'buffer' } });
        resolveRecv(h);
      });

      const t0 = performance.now();
      const handle = await rig.alice.upload({
        to: 'bob',
        input,
        metadata: { name: 'failover.bin' },
      });
      const recvHandle = await recvHandlePromise;
      const [senderResult, recvResult] = await Promise.all([
        handle.done(),
        recvHandle.done(),
      ]);
      const elapsed = performance.now() - t0;
      unsubscribe();

      const received =
        (recvResult as TransferResult & { bytes?: Uint8Array }).bytes ?? new Uint8Array();
      expect(received).toEqual(input);
      expect(senderResult.sha256).toBe(hex(sha256Once(input)));

      const runtime = rig.alice.getWebRtcRuntime();
      expect(runtime!.fallback.activeName).toBe('http');
      expect(runtime!.fallback.hasFallenBack).toBe(true);
      expect(runtime!.fallback.failures.length).toBeGreaterThanOrEqual(1);

      // V3.11 acceptance: failover within 5 s.
      expect(elapsed).toBeLessThan(5_000);
    },
    15_000,
  );
});
