import { describe, expect, it } from 'bun:test';
import {
  MultiTransportFallback,
  TransferTransportError,
  type ChunkAck,
  type ITransferTransport,
  type TransferResumeState,
} from '../src/index.js';

class StubTransport implements ITransferTransport {
  callCount = 0;
  constructor(
    private readonly behavior: () => 'ok' | 'transport-error' | 'other-error',
  ) {}
  async probe(): Promise<void> {
    this.callCount++;
    const verdict = this.behavior();
    if (verdict === 'transport-error') throw new TransferTransportError('probe failed');
    if (verdict === 'other-error') throw new Error('other');
  }
  async sendChunk(
    _peer: string,
    _streamId: string,
    _laneId: number,
    seq: number | bigint,
  ): Promise<ChunkAck> {
    this.callCount++;
    const verdict = this.behavior();
    if (verdict === 'transport-error') throw new TransferTransportError('send failed');
    if (verdict === 'other-error') throw new Error('other');
    return { lastSeq: typeof seq === 'bigint' ? Number(seq) : seq };
  }
  async fetchResumeState(): Promise<TransferResumeState | null> {
    this.callCount++;
    const verdict = this.behavior();
    if (verdict === 'transport-error') throw new TransferTransportError('resume failed');
    return null;
  }
}

describe('MultiTransportFallback', () => {
  it('uses the primary transport when probe succeeds', async () => {
    const primary = new StubTransport(() => 'ok');
    const secondary = new StubTransport(() => 'ok');
    const tertiary = new StubTransport(() => 'ok');
    const fb = new MultiTransportFallback([
      { name: 'webrtc', transport: primary },
      { name: 'ws', transport: secondary },
      { name: 'http', transport: tertiary },
    ]);
    await fb.probe('bob');
    expect(fb.activeName).toBe('webrtc');
    expect(primary.callCount).toBe(1);
    expect(secondary.callCount).toBe(0);
  });

  it('demotes through layers on transport errors', async () => {
    const primary = new StubTransport(() => 'transport-error');
    const secondary = new StubTransport(() => 'transport-error');
    const tertiary = new StubTransport(() => 'ok');
    const fb = new MultiTransportFallback([
      { name: 'webrtc', transport: primary },
      { name: 'ws', transport: secondary },
      { name: 'http', transport: tertiary },
    ]);
    const switches: Array<{ from: string; to: string }> = [];
    fb.onSwitch((from, to) => switches.push({ from, to }));
    await fb.probe('bob');
    expect(fb.activeName).toBe('http');
    expect(switches).toEqual([
      { from: 'webrtc', to: 'ws' },
      { from: 'ws', to: 'http' },
    ]);
    expect(fb.failures).toHaveLength(2);
  });

  it('throws if every layer fails', async () => {
    const fb = new MultiTransportFallback([
      { name: 'a', transport: new StubTransport(() => 'transport-error') },
      { name: 'b', transport: new StubTransport(() => 'transport-error') },
    ]);
    await expect(fb.probe('bob')).rejects.toThrow();
  });

  it('does NOT demote on non-transport errors', async () => {
    const primary = new StubTransport(() => 'other-error');
    const fb = new MultiTransportFallback([
      { name: 'p', transport: primary },
      { name: 's', transport: new StubTransport(() => 'ok') },
    ]);
    await expect(fb.probe('bob')).rejects.toThrow(/other/);
    // We did NOT advance — non-transport errors are caller bugs.
    expect(fb.activeName).toBe('p');
  });

  it('sticks to the demoted layer for sendChunk after probe failure', async () => {
    const primary = new StubTransport(() => 'transport-error');
    const secondary = new StubTransport(() => 'ok');
    const fb = new MultiTransportFallback([
      { name: 'p', transport: primary },
      { name: 's', transport: secondary },
    ]);
    await fb.probe('bob');
    expect(fb.activeName).toBe('s');
    // primary not called again
    const before = primary.callCount;
    await fb.sendChunk('bob', 'sid', 0, 0n, new Uint8Array(8));
    expect(primary.callCount).toBe(before);
    expect(secondary.callCount).toBeGreaterThan(0);
  });

  it('rejects empty transport list', () => {
    expect(() => new MultiTransportFallback([])).toThrow();
  });
});
