import {
  afterEach,
  beforeEach,
  describe,
  expect,
  it,
  MockInstance,
  vi,
} from 'vitest';
import { fromPartial } from '@total-typescript/shoehorn';
import { StreamVideoClient } from '../StreamVideoClient';
import 'dotenv/config';
import { StreamClient } from '@stream-io/node-sdk';
import { generateUUIDv4, sleep } from '../coordinator/connection/utils';
import { CallingState } from '../store';
import { Dispatcher } from '../rtc';
import { Call } from '../Call';
import { StreamVideoParticipant } from '../types';
import { TrackType } from '../gen/video/sfu/models/models';

const apiKey = process.env.STREAM_API_KEY!;
const secret = process.env.STREAM_SECRET!;
const serverClient = new StreamClient(apiKey, secret);
const userId = 'jane';
const tokenProvider = async () =>
  serverClient.generateUserToken({ user_id: userId, role: 'user' });

let client: StreamVideoClient;

beforeEach(async () => {
  client = new StreamVideoClient({
    apiKey,
    options: { browser: true, timeout: 15000 },
    tokenProvider,
    user: { id: 'jane' },
  });

  await sleep(50);
  await client.streamClient.wsPromise;
});

it('can get a call', async () => {
  const call = client.call('default', generateUUIDv4());
  expect(call.watching).toBeFalsy();
  await call.getOrCreate();
  expect(call.watching).toBeTruthy();
  await call.leave();
  expect(call.state.callingState).toBe(CallingState.LEFT);
});

it('can reuse call instance', async () => {
  const call = client.call('default', generateUUIDv4());
  await call.getOrCreate();
  expect(call.state.callingState).toBe(CallingState.IDLE);
  await call.leave();
  expect(call.state.callingState).toBe(CallingState.LEFT);
  await call.get();
  expect(call.state.callingState).toBe(CallingState.IDLE);
  await call.leave();
  expect(call.state.callingState).toBe(CallingState.LEFT);
});

it('stops reacting to events when not watching', async () => {
  const call = client.call('default', generateUUIDv4());
  await call.getOrCreate();
  expect(call.state.transcribing).toBeFalsy();
  call.streamClient.dispatchEvent(
    fromPartial({
      type: 'call.transcription_started',
      call_cid: call.cid,
      created_at: new Date().toISOString(),
    }),
  );
  expect(call.state.transcribing).toBeTruthy();
  await call.leave();
  call.streamClient.dispatchEvent(
    fromPartial({
      type: 'call.transcription_stopped',
      call_cid: call.cid,
      created_at: new Date().toISOString(),
    }),
  );
  expect(call.state.transcribing).toBeTruthy();
});

it('keeps user handlers for SFU and coordinator events', async () => {
  const call = client.call('default', generateUUIDv4());
  const sfuEventHandler = vi.fn();
  const coordinatorEventHandler = vi.fn();
  call.on('participantJoined', sfuEventHandler);
  call.on('call.transcription_started', coordinatorEventHandler);
  await call.getOrCreate();
  await call.leave();
  (call as unknown as { dispatcher: Dispatcher }).dispatcher.dispatch({
    eventPayload: {
      oneofKind: 'participantJoined',
      participantJoined: {
        callCid: call.cid,
      },
    },
  });
  call.streamClient.dispatchEvent(
    fromPartial({
      type: 'call.transcription_started',
      call_cid: call.cid,
      created_at: new Date().toISOString(),
    }),
  );
  expect(sfuEventHandler).toBeCalled();
  expect(coordinatorEventHandler).toBeCalled();
});

it("doesn't break when joining and leaving the same instance in quick succession", async () => {
  const call = client.call('default', generateUUIDv4());
  const states: CallingState[] = [];
  call.state.callingState$.subscribe((state) => states.push(state));
  call.getOrCreate();
  call.leave();
  call.getOrCreate();
  call.leave();
  call.getOrCreate();
  await call.leave();
  expect(states).toMatchObject([
    'idle',
    'left',
    'idle',
    'left',
    'idle',
    'left',
  ]);
});

describe('state updates in reponse to coordinator API', () => {
  let call: Call;

  beforeEach(() => {
    call = client.call('default', generateUUIDv4());
  });

  it('should create and update state', async () => {
    await call.create({
      data: {
        members: [{ user_id: 'sara' }],
        settings_override: {
          screensharing: {
            enabled: false,
          },
          audio: {
            default_device: 'earpiece',
          },
        },
      },
    });

    expect(call.state.settings?.screensharing.enabled).toBe(false);
    expect(call.state.settings?.audio.default_device).toBe('earpiece');
    expect(call.state.members.length).toBe(1);
    expect(call.state.members[0].user_id).toBe('sara');
    expect(call.isCreatedByMe).toBe(true);
  });

  it('should get or create and update state', async () => {
    await call.create({
      data: {
        members: [{ user_id: 'sara', role: 'admin' }],
        settings_override: {
          limits: {
            max_participants: 5,
          },
        },
      },
    });

    expect(call.state.settings?.limits.max_participants).toBe(5);
    expect(call.state.members.length).toBe(1);
    expect(call.state.members[0].user_id).toBe('sara');
    expect(call.state.members[0].role).toBe('admin');
  });

  it('should get or create and update state', async () => {
    await call.getOrCreate({
      data: {
        members: [{ user_id: 'sara', role: 'admin' }],
        settings_override: {
          limits: {
            max_participants: 5,
          },
        },
      },
    });

    expect(call.state.settings?.limits.max_participants).toBe(5);
    expect(call.state.members.length).toBe(1);
    expect(call.state.members[0].user_id).toBe('sara');
    expect(call.state.members[0].role).toBe('admin');
  });

  it('should get and update state', async () => {
    await serverClient.video.call(call.type, call.id).create({
      data: {
        settings_override: {
          limits: { max_duration_seconds: 180 },
        },
        created_by_id: userId,
      },
    });

    await call.get();

    expect(call.state.settings?.limits.max_duration_seconds).toBe(180);
  });

  it('should ring', async () => {
    await call.getOrCreate({ ring: true });

    expect(call.ringing).toBe(true);

    await call.leave();
  });

  it('should query call members', async () => {
    await call.getOrCreate({
      data: {
        members: [
          { user_id: 'sara', role: 'admin' },
          { user_id: 'jane' },
          { user_id: 'jack' },
        ],
      },
    });

    // default sorting
    let result = await call.queryMembers();

    expect(result.members.length).toBe(3);

    // sorting and pagination
    const queryMembersReq = {
      sort: [{ field: 'user_id', direction: 1 }],
      limit: 2,
    };
    result = await call.queryMembers(queryMembersReq);

    expect(result.members.length).toBe(2);
    expect(result.members[0].user_id).toBe('jack');
    expect(result.members[1].user_id).toBe('jane');

    // loading next page
    result = await call.queryMembers({ ...queryMembersReq, next: result.next });

    expect(result.members.length).toBe(1);

    result = await call.queryMembers({
      filter_conditions: { role: { $eq: 'admin' } },
    });

    expect(result.members.length).toBe(1);
    expect(result.members[0].user_id).toBe('sara');
  });

  afterEach(async () => {
    await serverClient.video.call(call.type, call.id).delete({ hard: true });
  });
});

describe('muting logic', () => {
  let call: Call;
  let spy: MockInstance;

  beforeEach(async () => {
    call = client.call('default', generateUUIDv4());
    call.state.updateOrAddParticipant('1', {
      userId: 'sara',
      publishedTracks: [TrackType.AUDIO],
    } as StreamVideoParticipant);

    spy = vi
      .spyOn(call, 'muteUser')
      .mockImplementation(() => Promise.resolve({ duration: '0ms' }));
  });

  it('should mute self', async () => {
    await call.muteSelf('audio');

    expect(spy).toHaveBeenCalledWith(userId, 'audio');
  });

  it('should mute others', () => {
    call.muteOthers('video');

    expect(spy).not.toHaveBeenCalled();

    call.muteOthers('audio');

    expect(spy).toHaveBeenCalledWith(['sara'], 'audio');
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });
});

afterEach(() => {
  client.disconnectUser();
});
