import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request";
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
import { DeviceType } from "@bitwarden/common/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { MasterKey, PrfKey, UserKey } from "@bitwarden/common/types/key";

import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-login.strategy";
import { PasswordLoginStrategyData } from "../../login-strategies/password-login.strategy";
import { SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy";
import { UserApiLoginStrategyData } from "../../login-strategies/user-api-login.strategy";
import { WebAuthnLoginStrategyData } from "../../login-strategies/webauthn-login.strategy";
import {
  MockAuthenticatorAssertionResponse,
  MockPublicKeyCredential,
} from "../../login-strategies/webauthn-login.strategy.spec";
import { AuthRequestLoginCredentials, WebAuthnLoginCredentials } from "../../models";

import { CACHE_KEY } from "./login-strategy.state";

describe("LOGIN_STRATEGY_CACHE_KEY", () => {
  const sut = CACHE_KEY;

  let deviceRequest: DeviceRequest;
  let twoFactorRequest: TokenTwoFactorRequest;

  beforeEach(() => {
    deviceRequest = Object.assign(Object.create(DeviceRequest.prototype), {
      type: DeviceType.ChromeBrowser,
      name: "DEVICE_NAME",
      identifier: "DEVICE_IDENTIFIER",
      pushToken: "PUSH_TOKEN",
    });

    twoFactorRequest = new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false);
  });

  it("should correctly deserialize PasswordLoginStrategyData", () => {
    const actual = {
      password: new PasswordLoginStrategyData(),
    };
    actual.password.tokenRequest = new PasswordTokenRequest(
      "EMAIL",
      "LOCAL_PASSWORD_HASH",
      "CAPTCHA_TOKEN",
      twoFactorRequest,
      deviceRequest,
    );
    actual.password.masterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey;
    actual.password.localMasterKeyHash = "LOCAL_MASTER_KEY_HASH";

    const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));

    expect(result.password).toBeInstanceOf(PasswordLoginStrategyData);
    verifyPropertyPrototypes(result, actual);
  });

  it("should correctly deserialize SsoLoginStrategyData", () => {
    const actual = { sso: new SsoLoginStrategyData() };
    actual.sso.tokenRequest = new SsoTokenRequest(
      "CODE",
      "CODE_VERIFIER",
      "REDIRECT_URI",
      twoFactorRequest,
      deviceRequest,
    );

    const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));

    expect(result.sso).toBeInstanceOf(SsoLoginStrategyData);
    verifyPropertyPrototypes(result, actual);
  });

  it("should correctly deserialize UserApiLoginStrategyData", () => {
    const actual = { userApiKey: new UserApiLoginStrategyData() };
    actual.userApiKey.tokenRequest = new UserApiTokenRequest("CLIENT_ID", "CLIENT_SECRET", null);

    const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));

    expect(result.userApiKey).toBeInstanceOf(UserApiLoginStrategyData);
    verifyPropertyPrototypes(result, actual);
  });

  it("should correctly deserialize AuthRequestLoginStrategyData", () => {
    const actual = { authRequest: new AuthRequestLoginStrategyData() };
    actual.authRequest.tokenRequest = new PasswordTokenRequest("EMAIL", "ACCESS_CODE", null, null);
    actual.authRequest.authRequestCredentials = new AuthRequestLoginCredentials(
      "EMAIL",
      "ACCESS_CODE",
      "AUTH_REQUEST_ID",
      new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
      new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey,
      "MASTER_KEY_HASH",
    );

    const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));

    expect(result.authRequest).toBeInstanceOf(AuthRequestLoginStrategyData);
    verifyPropertyPrototypes(result, actual);
  });

  it("should correctly deserialize WebAuthnLoginStrategyData", () => {
    global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse;
    const actual = { webAuthn: new WebAuthnLoginStrategyData() };
    const publicKeyCredential = new MockPublicKeyCredential();
    const deviceResponse = new WebAuthnLoginAssertionResponseRequest(publicKeyCredential);
    const prfKey = new SymmetricCryptoKey(new Uint8Array(64)) as PrfKey;
    actual.webAuthn.credentials = new WebAuthnLoginCredentials("TOKEN", deviceResponse, prfKey);
    actual.webAuthn.tokenRequest = new WebAuthnLoginTokenRequest(
      "TOKEN",
      deviceResponse,
      deviceRequest,
    );
    actual.webAuthn.captchaBypassToken = "CAPTCHA_BYPASS_TOKEN";
    actual.webAuthn.tokenRequest.setTwoFactor(
      new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false),
    );

    const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));

    expect(result.webAuthn).toBeInstanceOf(WebAuthnLoginStrategyData);
    verifyPropertyPrototypes(result, actual);
  });
});

/**
 * Recursively verifies the prototypes of all objects in the deserialized object.
 * It is important that the concrete object has the correct prototypes for
 * comparison.
 * @param deserialized the deserialized object
 * @param concrete the object stored in state
 */
function verifyPropertyPrototypes(deserialized: object, concrete: object) {
  for (const key of Object.keys(deserialized)) {
    const deserializedProperty = (deserialized as any)[key];
    if (deserializedProperty === undefined) {
      continue;
    }
    const realProperty = (concrete as any)[key];
    if (realProperty === undefined) {
      throw new Error(`Expected ${key} to be defined in ${concrete.constructor.name}`);
    }
    // we only care about checking prototypes of objects
    if (typeof realProperty === "object" && realProperty !== null) {
      const realProto = Object.getPrototypeOf(realProperty);
      expect(deserializedProperty).toBeInstanceOf(realProto.constructor);
      verifyPropertyPrototypes(deserializedProperty, realProperty);
    }
  }
}
