import { describe, expect, it, vi } from 'vitest';
import { fromPartial } from '@total-typescript/shoehorn';
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
import {
  watchCallAccepted,
  watchCallEnded,
  watchCallRejected,
  watchSfuCallEnded,
} from '../call';
import {
  CallAcceptedEvent,
  CallEndedEvent,
  CallResponse,
  OwnCapability,
  RejectCallResponse,
} from '../../gen/coordinator';
import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { SfuEvent } from '../../gen/video/sfu/event/events';
import { CallEndedReason } from '../../gen/video/sfu/models/models';

describe('Call ringing events', () => {
  describe(`call.accepted`, () => {
    it(`will ignore events from the current user`, async () => {
      const call = fakeCall();
      vi.spyOn(call, 'join');
      const handler = watchCallAccepted(call);
      const event: CallAcceptedEvent = {
        type: 'call.accepted',
        // @ts-expect-error incomplete data
        user: { id: 'test-user-id' },
      };
      await handler(event);

      expect(call.join).not.toHaveBeenCalled();
    });

    it(`will join the call for the caller if atleast one callee has accepted`, async () => {
      const call = fakeCall({ currentUserId: 'test-user' });
      vi.spyOn(call, 'join').mockImplementation(async () => {
        console.log(`TEST: join() called`);
      });
      const handler = watchCallAccepted(call);
      const event: CallAcceptedEvent = {
        type: 'call.accepted',
        // @ts-expect-error incomplete data
        user: { id: 'test-user-id-callee' },
        // @ts-expect-error incomplete data
        call: { created_by: { id: 'test-user' } },
      };
      await handler(event);

      expect(call.join).toHaveBeenCalled();
    });
  });

  it('will not join the call for the other callee automatically when someone accepts', async () => {
    const call = fakeCall({ currentUserId: 'test-user-id-callee-2' });
    vi.spyOn(call, 'join').mockImplementation(async () => {
      console.log(`TEST: join() called`);
    });
    const handler = watchCallAccepted(call);
    const event: CallAcceptedEvent = {
      type: 'call.accepted',
      // @ts-expect-error incomplete data
      user: { id: 'test-user-id-callee-1' },
      // @ts-expect-error incomplete data
      call: { created_by: { id: 'test-user-id-caller' } },
    };

    await handler(event);

    expect(call.join).not.toHaveBeenCalled();
  });

  describe(`call.rejected`, () => {
    it(`caller will leave the call if all callees have rejected`, async () => {
      const call = fakeCall({ currentUserId: 'm1' });
      call.state.updateFromCallResponse({
        ...fakeMetadata(),
        // @ts-expect-error type issue
        created_by: { id: 'm1' },
      });
      call.state.setMembers([
        // @ts-expect-error incomplete data
        { user_id: 'm1' },
        // @ts-expect-error incomplete data
        { user_id: 'm2' },
        // @ts-expect-error incomplete data
        { user_id: 'm3' },
      ]);
      call.state.setCallingState(CallingState.RINGING);
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });

      const handler = watchCallRejected(call);
      // all members reject the call
      await handler({
        type: 'call.rejected',
        // @ts-expect-error type issue
        user: {
          id: 'm2',
        },
        call: {
          // @ts-expect-error type issue
          created_by: {
            id: 'm1',
          },
          // @ts-expect-error type issue
          session: {
            rejected_by: {
              m2: new Date().toISOString(),
              m3: new Date().toISOString(),
            },
          },
        },
      });
      expect(call.leave).toHaveBeenCalledWith({
        reject: true,
        reason: 'cancel',
        message: 'ring: everyone rejected',
      });
    });

    it(`caller will not leave the call if only one callee rejects`, async () => {
      const call = fakeCall();
      call.state.updateFromCallResponse({
        ...fakeMetadata(),
        // @ts-expect-error type issue
        created_by: { id: 'm0' },
      });
      // @ts-expect-error incomplete data
      call.state.setMembers([{ user_id: 'm1' }, { user_id: 'm2' }]);
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });
      const handler = watchCallRejected(call);

      // only one member rejects the call
      const event: CallAcceptedEvent = {
        type: 'call.rejected',
        // @ts-expect-error type issue
        user: {
          id: 'm2',
        },
        call: {
          // @ts-expect-error type issue
          created_by: {
            id: 'm0',
          },
          // @ts-expect-error type issue
          session: {
            rejected_by: {
              m2: new Date().toISOString(),
            },
          },
        },
      };
      await handler(event);

      expect(call.leave).not.toHaveBeenCalled();
    });

    it('callee will leave the call if caller rejects', async () => {
      const call = fakeCall({ currentUserId: 'm1' });
      call.state.updateFromCallResponse({
        ...fakeMetadata(),
        // @ts-expect-error type issue
        created_by: { id: 'm0' },
      });
      // @ts-expect-error incomplete data
      call.state.setMembers([{ user_id: 'm1' }, { user_id: 'm2' }]);
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });
      const handler = watchCallRejected(call);

      // only one member rejects the call
      const event: CallAcceptedEvent = {
        type: 'call.rejected',
        // @ts-expect-error type issue
        user: {
          id: 'm0',
        },
        call: {
          // @ts-expect-error type issue
          created_by: {
            id: 'm0',
          },
          // @ts-expect-error type issue
          session: {
            rejected_by: {
              m0: new Date().toISOString(),
            },
          },
        },
      };
      await handler(event);

      expect(call.leave).toHaveBeenCalled();
    });
  });

  describe(`call.ended`, () => {
    it(`will leave the call unless joined`, async () => {
      const call = fakeCall();
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });
      const handler = watchCallEnded(call);

      // @ts-expect-error type issue
      const event: CallEndedEvent = { type: 'call.ended' };
      // @ts-expect-error type issue
      await handler(event);

      expect(call.leave).toHaveBeenCalled();
    });

    it(`will leave the call if joined`, async () => {
      const call = fakeCall();
      vi.spyOn(call, 'join').mockImplementation(async () => {
        console.log(`TEST: join() called`);
        call.state.setCallingState(CallingState.JOINED);
      });
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });

      await call.join();

      const handler = watchCallEnded(call);

      // @ts-expect-error type issue
      const event: CallEndedEvent = { type: 'call.ended' };
      // @ts-expect-error type issue
      await handler(event);

      expect(call.leave).toHaveBeenCalled();
    });

    it(`will not leave the call if idle`, async () => {
      const call = fakeCall({ ring: false });
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });

      const handler = watchCallEnded(call);

      // @ts-expect-error type issue
      const event: CallEndedEvent = { type: 'call.ended' };
      // @ts-expect-error type issue
      await handler(event);

      expect(call.leave).not.toHaveBeenCalled();
    });
  });

  describe('callEnded (SFU)', () => {
    it('will leave the call if not already left', async () => {
      const call = fakeCall();
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });

      watchSfuCallEnded(call);
      const event: SfuEvent = {
        eventPayload: {
          oneofKind: 'callEnded',
          callEnded: { reason: CallEndedReason.ENDED },
        },
      };
      // @ts-expect-error type issue
      call['dispatcher'].dispatch(event);

      expect(call.leave).toHaveBeenCalled();
      expect(call.state.endedAt).toBeDefined();
    });

    it('will not leave the call if already left', async () => {
      const call = fakeCall();
      call.state.setCallingState(CallingState.LEFT);
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });

      watchSfuCallEnded(call);
      const event: SfuEvent = {
        eventPayload: {
          oneofKind: 'callEnded',
          callEnded: { reason: CallEndedReason.KICKED },
        },
      };
      // @ts-expect-error type issue
      call['dispatcher'].dispatch(event);

      expect(call.leave).not.toHaveBeenCalled();
    });

    it('will stay in backstage if live ended and has permission', async () => {
      const call = fakeCall();
      call.state.setBackstage(false);
      call.permissionsContext.setPermissions([OwnCapability.JOIN_BACKSTAGE]);
      vi.spyOn(call, 'leave').mockImplementation(async () => {
        console.log(`TEST: leave() called`);
      });

      watchSfuCallEnded(call);
      const event: SfuEvent = {
        eventPayload: {
          oneofKind: 'callEnded',
          callEnded: { reason: CallEndedReason.LIVE_ENDED },
        },
      };
      // @ts-expect-error type issue
      call['dispatcher'].dispatch(event);

      expect(call.leave).not.toHaveBeenCalled();
      expect(call.state.backstage).toBe(true);
    });
  });

  describe('call.leave', () => {
    it('should not call reject when leaving under specific conditions', async () => {
      const call = fakeCall();
      call.state.setCallingState(CallingState.JOINED);
      const rejectSpy = vi
        .spyOn(call, 'reject')
        .mockImplementation(async () => {
          console.log('TEST: reject() called');
          return {} as RejectCallResponse;
        });

      await call.leave({ reject: false });

      expect(rejectSpy).not.toHaveBeenCalled();
    });

    it('should call reject when leaving while ringing and reject is true', async () => {
      const call = fakeCall();
      call.state.setCallingState(CallingState.RINGING);
      const rejectSpy = vi
        .spyOn(call, 'reject')
        .mockImplementation(async () => {
          console.log('TEST: reject() called');
          return {} as RejectCallResponse;
        });

      await call.leave({ reject: true });

      expect(rejectSpy).toHaveBeenCalled();
    });
  });
});

const fakeCall = ({ ring = true, currentUserId = 'test-user-id' } = {}) => {
  const store = new StreamVideoWriteableStateStore();
  store.setConnectedUser({
    id: currentUserId,
    created_at: '',
    updated_at: '',
    role: '',
    custom: {},
    teams: [],
    devices: [],
    language: '',
  });
  const client = new StreamClient('api-key');
  return new Call({
    type: 'development',
    id: '12345',
    clientStore: store,
    streamClient: client,
    ringing: ring,
  });
};

const fakeMetadata = (): CallResponse => {
  return fromPartial({
    id: '12345',
    type: 'development',
    cid: 'development:12345',

    created_by: {
      id: 'test-user-id',
    },
    blocked_user_ids: [],
    egress: {},

    settings: {
      ring: {
        auto_cancel_timeout_ms: 30000,
        incoming_call_timeout_ms: 30000,
        missed_call_timeout_ms: 30000,
      },
      screensharing: {
        target_resolution: undefined,
      },
    },
  });
};
