import { describe, expect, it } from 'vitest';
import { ScreenSharePresets, VideoPreset, VideoPresets, VideoPresets43 } from '../track/options';
import {
  computeDefaultScreenShareSimulcastPresets,
  computeStartTargetBitrate,
  computeVideoEncodings,
  determineAppropriateEncoding,
  presets43,
  presets169,
  presetsForResolution,
  presetsScreenShare,
  sortPresets,
} from './publishUtils';

describe('presetsForResolution', () => {
  it('handles screenshare', () => {
    expect(presetsForResolution(true, 600, 300)).toEqual(presetsScreenShare);
  });

  it('handles landscape', () => {
    expect(presetsForResolution(false, 600, 300)).toEqual(presets169);
    expect(presetsForResolution(false, 500, 500)).toEqual(presets43);
  });

  it('handles portrait', () => {
    expect(presetsForResolution(false, 300, 600)).toEqual(presets169);
    expect(presetsForResolution(false, 500, 500)).toEqual(presets43);
  });
});

describe('determineAppropriateEncoding', () => {
  it('uses higher encoding', () => {
    expect(determineAppropriateEncoding(false, 600, 300)).toEqual(VideoPresets.h360.encoding);
  });

  it('handles portrait', () => {
    expect(determineAppropriateEncoding(false, 300, 600)).toEqual(VideoPresets.h360.encoding);
  });
});

describe('computeVideoEncodings', () => {
  it('handles non-simulcast', () => {
    const encodings = computeVideoEncodings(false, 640, 480, {
      simulcast: false,
    });
    expect(encodings).toEqual([{}]);
  });

  it('respects client defined bitrate', () => {
    const encodings = computeVideoEncodings(false, 640, 480, {
      simulcast: false,
      videoEncoding: {
        maxBitrate: 1024,
      },
    });
    expect(encodings).toHaveLength(1);
    expect(encodings![0].maxBitrate).toBe(1024);
  });

  it('returns three encodings for high-res simulcast', () => {
    const encodings = computeVideoEncodings(false, 960, 540, {
      simulcast: true,
    });
    expect(encodings).toHaveLength(3);

    // ensure they are what we expect
    expect(encodings![0].rid).toBe('q');
    expect(encodings![0].maxBitrate).toBe(VideoPresets.h180.encoding.maxBitrate);
    expect(encodings![0].scaleResolutionDownBy).toBe(3);
    expect(encodings![1].rid).toBe('h');
    expect(encodings![1].scaleResolutionDownBy).toBe(1.5);
    expect(encodings![2].rid).toBe('f');
  });

  it('handles portrait simulcast', () => {
    const encodings = computeVideoEncodings(false, 540, 960, {
      simulcast: true,
    });
    expect(encodings).toHaveLength(3);
    expect(encodings![0].scaleResolutionDownBy).toBe(3);
    expect(encodings![1].scaleResolutionDownBy).toBe(1.5);
    expect(encodings![2].maxBitrate).toBe(VideoPresets.h540.encoding.maxBitrate);
  });

  it('returns two encodings for lower-res simulcast', () => {
    const encodings = computeVideoEncodings(false, 640, 360, {
      simulcast: true,
    });
    expect(encodings).toHaveLength(2);

    // ensure they are what we expect
    expect(encodings![0].rid).toBe('q');
    expect(encodings![0].maxBitrate).toBe(VideoPresets.h180.encoding.maxBitrate);
    expect(encodings![1].rid).toBe('h');
    expect(encodings![1].maxBitrate).toBe(VideoPresets.h360.encoding.maxBitrate);
  });

  it('returns one encoding if an empty array is provided for custom screen share layers', () => {
    const encodings = computeVideoEncodings(true, 1920, 1080, {
      simulcast: true,
      screenShareSimulcastLayers: [],
    });
    expect(encodings).toHaveLength(1);

    // ensure they are what we expect
    expect(encodings![0].rid).toBe('q');
    expect(encodings![0].scaleResolutionDownBy).toBe(1);
  });

  it('respects provided min resolution', () => {
    const encodings = computeVideoEncodings(false, 100, 120, {
      simulcast: true,
    });
    expect(encodings).toHaveLength(1);
    expect(encodings![0].rid).toBe('q');
    expect(encodings![0].maxBitrate).toBe(VideoPresets43.h120.encoding.maxBitrate);
    expect(encodings![0].scaleResolutionDownBy).toBe(1);
  });

  // svc carries the scalabilityMode on the first encoding only (whether it emits a
  // single encoding or the legacy multi-encoding shape), simulcast carries it on all
  const countScalabilityModes = (encodings?: RTCRtpEncodingParameters[]) =>
    /* @ts-ignore */
    encodings!.filter((encoding) => encoding.scalabilityMode !== undefined).length;

  it('keeps svc for an svc codec without simulcast', () => {
    const encodings = computeVideoEncodings(false, 960, 540, {
      simulcast: false,
      videoCodec: 'vp9',
      scalabilityMode: 'L3T3_KEY',
    });
    /* @ts-ignore */
    expect(encodings![0].scalabilityMode).toBe('L3T3_KEY');
    expect(countScalabilityModes(encodings)).toBe(1);
  });

  it('keeps svc for an svc codec with a multi spatial layer mode even if simulcast is set', () => {
    const encodings = computeVideoEncodings(false, 960, 540, {
      simulcast: true,
      videoCodec: 'vp9',
      scalabilityMode: 'L3T3_KEY',
    });
    /* @ts-ignore */
    expect(encodings![0].scalabilityMode).toBe('L3T3_KEY');
    expect(countScalabilityModes(encodings)).toBe(1);
  });

  it('returns a simulcast ladder for an svc codec with simulcast and an L1Tx mode', () => {
    for (const videoCodec of ['vp9', 'av1'] as const) {
      const encodings = computeVideoEncodings(false, 960, 540, {
        simulcast: true,
        videoCodec,
        scalabilityMode: 'L1T2',
      });
      expect(encodings).toHaveLength(3);
      expect(encodings!.map((e) => e.rid)).toEqual(['q', 'h', 'f']);
      // every encoding needs both scalabilityMode and scaleResolutionDownBy for chrome
      // M113+ to treat them as real simulcast rather than legacy svc
      encodings!.forEach((encoding) => {
        /* @ts-ignore */
        expect(encoding.scalabilityMode).toBe('L1T2');
        expect(encoding.scaleResolutionDownBy).toBeGreaterThanOrEqual(1);
      });
    }
  });

  it('sets the scalability mode on a single encoding svc simulcast ladder', () => {
    const encodings = computeVideoEncodings(false, 100, 120, {
      simulcast: true,
      videoCodec: 'vp9',
      scalabilityMode: 'L1T3',
    });
    expect(encodings).toHaveLength(1);
    expect(encodings![0].rid).toBe('q');
    /* @ts-ignore */
    expect(encodings![0].scalabilityMode).toBe('L1T3');
  });

  it('does not set a scalability mode for non-svc simulcast', () => {
    const encodings = computeVideoEncodings(false, 960, 540, {
      simulcast: true,
      videoCodec: 'vp8',
      scalabilityMode: 'L1T2',
    });
    expect(encodings).toHaveLength(3);
    encodings!.forEach((encoding) => {
      /* @ts-ignore */
      expect(encoding.scalabilityMode).toBeUndefined();
    });
  });

  //   it('respects default backup codec encoding', () => {
  //     const vp8Encodings = computeTrackBackupEncodings(false, 100, 120, { simulcast: true });
  //     const h264Encodings = computeVideoEncodings(false, 100, 120, {
  //       simulcast: true,
  //       videoCodec: 'h264',
  //     });
  //     const av1Encodings = computeVideoEncodings(false, 100, 120, {
  //       simulcast: true,
  //       videoCodec: 'av1',
  //     });
  //     expect(h264Encodings).toHaveLength(1);
  //     expect(h264Encodings![0].rid).toBe('q');
  //     expect(h264Encodings![0].maxBitrate).toBe(vp8Encodings[0].maxBitrate! * 1.1);
  //     expect(av1Encodings![0].maxBitrate).toBe(vp8Encodings[0].maxBitrate! * 0.7);
  //     expect(h264Encodings![0].scaleResolutionDownBy).toBe(1);
  //   });

  //   it('respects custom backup codec encoding', () => {
  //     const encodings = computeVideoEncodings(false, 100, 120, {
  //       simulcast: true,
  //       videoCodec: 'h264',
  //       backupCodec: {
  //         vp8: { maxBitrate: 1_000 },
  //         h264: { maxBitrate: 2_000 },
  //       },
  //     });
  //     expect(encodings).toHaveLength(1);
  //     expect(encodings![0].rid).toBe('q');
  //     expect(encodings![0].maxBitrate).toBe(2_000);
  //     expect(encodings![0].scaleResolutionDownBy).toBe(1);
  //   });
});

describe('customSimulcastLayers', () => {
  it('sorts presets from lowest to highest', () => {
    const sortedPresets = sortPresets([
      VideoPresets.h1440,
      VideoPresets.h360,
      VideoPresets.h1080,
      VideoPresets.h90,
    ]) as Array<VideoPreset>;
    expect(sortPresets).not.toBeUndefined();
    expect(sortedPresets[0]).toBe(VideoPresets.h90);
    expect(sortedPresets[1]).toBe(VideoPresets.h360);
    expect(sortedPresets[2]).toBe(VideoPresets.h1080);
    expect(sortedPresets[3]).toBe(VideoPresets.h1440);
  });
  it('sorts presets from lowest to highest, even when dimensions are the same', () => {
    const sortedPresets = sortPresets([
      new VideoPreset(1920, 1080, 3_000_000, 20),
      new VideoPreset(1920, 1080, 2_000_000, 15),
      new VideoPreset(1920, 1080, 3_000_000, 15),
    ]) as Array<VideoPreset>;
    expect(sortPresets).not.toBeUndefined();
    expect(sortedPresets[0].encoding.maxBitrate).toBe(2_000_000);
    expect(sortedPresets[1].encoding.maxFramerate).toBe(15);
    expect(sortedPresets[2].encoding.maxFramerate).toBe(20);
  });
  it('does not mutate the passed-in array', () => {
    const presets = [VideoPresets.h1440, VideoPresets.h360, VideoPresets.h1080, VideoPresets.h90];
    const original = [...presets];
    sortPresets(presets);
    expect(presets).toEqual(original);
  });
});

describe('screenShareSimulcastDefaults', () => {
  it('computes appropriate bitrate from original preset', () => {
    const defaultSimulcastLayers = computeDefaultScreenShareSimulcastPresets(
      ScreenSharePresets.h720fps15,
    );
    expect(defaultSimulcastLayers[0].width).toBe(640);
    expect(defaultSimulcastLayers[0].height).toBe(360);
    expect(defaultSimulcastLayers[0].encoding.maxFramerate).toBe(15);
    expect(defaultSimulcastLayers[0].encoding.maxBitrate).toBe(375000);
  });
});

describe('computeStartTargetBitrate', () => {
  // ordered q..f, as encodingsFromPresets builds them
  const simulcastLadder: RTCRtpEncodingParameters[] = [
    { rid: 'q', maxBitrate: 160_000 },
    { rid: 'h', maxBitrate: 450_000 },
    { rid: 'f', maxBitrate: 680_000 },
  ];

  it('sums the ladder for plain simulcast', () => {
    expect(computeStartTargetBitrate('vp8', { simulcast: true }, simulcastLadder)).toBe(1_290_000);
  });

  it('sums the ladder for vp9/av1 published as rid simulcast', () => {
    // encodings[0] is the *smallest* layer here, so taking it would under-hint BWE
    for (const codec of ['vp9', 'av1'] as const) {
      expect(
        computeStartTargetBitrate(
          codec,
          { simulcast: true, videoCodec: codec, scalabilityMode: 'L1T2' },
          simulcastLadder,
        ),
      ).toBe(1_290_000);
    }
  });

  it('uses the single encoding for vp9/av1 svc', () => {
    for (const codec of ['vp9', 'av1'] as const) {
      expect(
        computeStartTargetBitrate(
          codec,
          { simulcast: false, videoCodec: codec, scalabilityMode: 'L3T3_KEY' },
          [{ maxBitrate: 680_000 }],
        ),
      ).toBe(680_000);
    }
  });

  it('uses the first encoding for the legacy svc shape, which is ordered f..q', () => {
    // legacy SVC pushes videoRids[2 - i], so encodings[0] carries the full bitrate
    const legacySvc: RTCRtpEncodingParameters[] = [
      { rid: 'f', maxBitrate: 680_000 },
      { rid: 'h', maxBitrate: 226_667 },
      { rid: 'q', maxBitrate: 75_556 },
    ];
    expect(
      computeStartTargetBitrate(
        'vp9',
        { simulcast: false, videoCodec: 'vp9', scalabilityMode: 'L3T3_KEY' },
        legacySvc,
      ),
    ).toBe(680_000);
  });

  it('handles missing bitrates and empty encodings', () => {
    expect(computeStartTargetBitrate('vp8', { simulcast: true }, [])).toBe(0);
    expect(computeStartTargetBitrate('vp9', undefined, [])).toBe(0);
    expect(computeStartTargetBitrate('vp8', { simulcast: true }, [{ rid: 'q' }])).toBe(0);
  });
});
