import { SignUpIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { HTTPError } from 'got';

import {
  mockSocialConnectorId,
  mockSocialConnectorTarget,
  mockSocialConnectorConfig,
} from '@/__mocks__/connectors-mock';
import {
  signInWithSocial,
  getAuthWithSocial,
  registerWithSocial,
  bindWithSocial,
  signInWithPassword,
  getUser,
} from '@/api';
import MockClient from '@/client';
import { setUpConnector, createUserByAdmin, setSignUpIdentifier } from '@/helpers';
import { generateUsername, generatePassword } from '@/utils';

const state = 'foo_state';
const redirectUri = 'http://foo.dev/callback';
const code = 'auth_code_foo';

describe('social sign-in and register', () => {
  const socialUserId = crypto.randomUUID();

  beforeAll(async () => {
    await setUpConnector(mockSocialConnectorId, mockSocialConnectorConfig);
    await setSignUpIdentifier(SignUpIdentifier.None, false);
  });

  it('register with social', async () => {
    const client = new MockClient();

    await client.initSession();
    assert(client.interactionCookie, new Error('Session not found'));

    await expect(
      signInWithSocial(
        { state, connectorId: mockSocialConnectorId, redirectUri },
        client.interactionCookie
      )
    ).resolves.toBeTruthy();

    const response = await getAuthWithSocial(
      {
        connectorId: mockSocialConnectorId,
        data: { state, redirectUri, code, userId: socialUserId },
      },
      client.interactionCookie
    ).catch((error: unknown) => error);

    // User with social does not exist
    expect(response instanceof HTTPError && response.response.statusCode === 422).toBe(true);

    // Register with social
    const { redirectTo } = await registerWithSocial(
      mockSocialConnectorId,
      client.interactionCookie
    );

    await client.processSession(redirectTo);

    await expect(client.isAuthenticated()).resolves.toBe(true);
  });

  /*
   * Note: As currently we can not prepare a social identities through admin api.
   * The sign-in test case MUST run concurrently after the register test case
   */
  it('Sign-In with social', async () => {
    const client = new MockClient();

    await client.initSession();
    assert(client.interactionCookie, new Error('Session not found'));

    await expect(
      signInWithSocial(
        { state, connectorId: mockSocialConnectorId, redirectUri },
        client.interactionCookie
      )
    ).resolves.toBeTruthy();

    const { redirectTo } = await getAuthWithSocial(
      {
        connectorId: mockSocialConnectorId,
        data: { state, redirectUri, code, userId: socialUserId },
      },
      client.interactionCookie
    );

    await client.processSession(redirectTo);

    await expect(client.isAuthenticated()).resolves.toBe(true);
  });
});

describe('social bind account', () => {
  const username = generateUsername();
  const password = generatePassword();

  beforeAll(async () => {
    await createUserByAdmin(username, password);
  });

  it('bind new social account', async () => {
    const client = new MockClient();

    await client.initSession();
    assert(client.interactionCookie, new Error('Session not found'));

    await expect(
      signInWithSocial(
        { state, connectorId: mockSocialConnectorId, redirectUri },
        client.interactionCookie
      )
    ).resolves.toBeTruthy();

    const response = await getAuthWithSocial(
      { connectorId: mockSocialConnectorId, data: { state, redirectUri, code } },
      client.interactionCookie
    ).catch((error: unknown) => error);

    // User with social does not exist
    expect(response instanceof HTTPError && response.response.statusCode === 422).toBe(true);

    const { redirectTo } = await signInWithPassword({
      username,
      password,
      interactionCookie: client.interactionCookie,
    });

    await expect(
      bindWithSocial(mockSocialConnectorId, client.interactionCookie)
    ).resolves.not.toThrow();

    await client.processSession(redirectTo);

    // User should bind with social identities
    const { sub } = await client.getIdTokenClaims();
    const user = await getUser(sub);

    expect(user.identities).toHaveProperty(mockSocialConnectorTarget);
  });
});
