import { mock } from "jest-mock-extended";
import { of } from "rxjs";

// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
  BiometricsService,
  BiometricsStatus,
  KdfConfig,
  KeyService,
  KdfConfigService,
} from "@bitwarden/key-management";

import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
import { PinLockType } from "../../../key-management/pin/pin-lock-type";
import { PinServiceAbstraction } from "../../../key-management/pin/pin.service.abstraction";
import { VaultTimeoutSettingsService } from "../../../key-management/vault-timeout";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { HashPurpose } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
import { VerificationType } from "../../enums/verification-type";
import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response";
import { MasterPasswordVerification } from "../../types/verification";

import { UserVerificationService } from "./user-verification.service";

describe("UserVerificationService", () => {
  let sut: UserVerificationService;

  const keyService = mock<KeyService>();
  const masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
  const i18nService = mock<I18nService>();
  const userVerificationApiService = mock<UserVerificationApiServiceAbstraction>();
  const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
  const pinService = mock<PinServiceAbstraction>();
  const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
  const kdfConfigService = mock<KdfConfigService>();
  const biometricsService = mock<BiometricsService>();

  const mockUserId = Utils.newGuid() as UserId;
  let accountService: FakeAccountService;

  beforeEach(() => {
    jest.clearAllMocks();
    accountService = mockAccountServiceWith(mockUserId);

    sut = new UserVerificationService(
      keyService,
      accountService,
      masterPasswordService,
      i18nService,
      userVerificationApiService,
      userDecryptionOptionsService,
      pinService,
      kdfConfigService,
      biometricsService,
    );
  });

  describe("getAvailableVerificationOptions", () => {
    describe("client verification type", () => {
      it("correctly returns master password availability", async () => {
        setMasterPasswordAvailability(true);
        setPinAvailability("DISABLED");
        disableBiometricsAvailability();

        const result = await sut.getAvailableVerificationOptions("client");

        expect(result).toEqual({
          client: {
            masterPassword: true,
            pin: false,
            biometrics: false,
          },
          server: {
            masterPassword: false,
            otp: false,
          },
        });
      });

      test.each([
        [true, "PERSISTENT"],
        [true, "EPHEMERAL"],
        [false, "DISABLED"],
      ])(
        "returns %s for PIN availability when pin lock type is %s",
        async (expectedPin: boolean, pinLockType: PinLockType) => {
          setMasterPasswordAvailability(false);
          setPinAvailability(pinLockType);
          disableBiometricsAvailability();

          const result = await sut.getAvailableVerificationOptions("client");

          expect(result).toEqual({
            client: {
              masterPassword: false,
              pin: expectedPin,
              biometrics: false,
            },
            server: {
              masterPassword: false,
              otp: false,
            },
          });
        },
      );

      test.each([
        [true, BiometricsStatus.Available],
        [false, BiometricsStatus.DesktopDisconnected],
        [false, BiometricsStatus.HardwareUnavailable],
      ])(
        "returns %s for biometrics availability when isBiometricLockSet is %s, hasUserKeyStored is %s, and supportsSecureStorage is %s",
        async (expectedReturn: boolean, biometricsStatus: BiometricsStatus) => {
          setMasterPasswordAvailability(false);
          setPinAvailability("DISABLED");
          biometricsService.getBiometricsStatus.mockResolvedValue(biometricsStatus);

          const result = await sut.getAvailableVerificationOptions("client");

          expect(result).toEqual({
            client: {
              masterPassword: false,
              pin: false,
              biometrics: expectedReturn,
            },
            server: {
              masterPassword: false,
              otp: false,
            },
          });
        },
      );
    });

    describe("server verification type", () => {
      it("correctly returns master password availability", async () => {
        userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));

        const result = await sut.getAvailableVerificationOptions("server");

        expect(result).toEqual({
          client: {
            masterPassword: false,
            pin: false,
            biometrics: false,
          },
          server: {
            masterPassword: true,
            otp: false,
          },
        });
      });

      it("correctly returns OTP availability", async () => {
        userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));

        const result = await sut.getAvailableVerificationOptions("server");

        expect(result).toEqual({
          client: {
            masterPassword: false,
            pin: false,
            biometrics: false,
          },
          server: {
            masterPassword: false,
            otp: true,
          },
        });
      });
    });
  });

  describe("buildRequest", () => {
    beforeEach(() => {
      accountService = mockAccountServiceWith(mockUserId);
      i18nService.t
        .calledWith("verificationCodeRequired")
        .mockReturnValue("Verification code is required");
      i18nService.t
        .calledWith("masterPasswordRequired")
        .mockReturnValue("Master Password is required");
    });

    describe("OTP verification", () => {
      it("should build request with OTP secret", async () => {
        const verification = {
          type: VerificationType.OTP,
          secret: "123456",
        } as any;

        const result = await sut.buildRequest(verification);

        expect(result.otp).toBe("123456");
      });

      it("should throw if OTP secret is empty", async () => {
        const verification = {
          type: VerificationType.OTP,
          secret: "",
        } as any;

        await expect(sut.buildRequest(verification)).rejects.toThrow(
          "Verification code is required",
        );
      });

      it("should throw if OTP secret is null", async () => {
        const verification = {
          type: VerificationType.OTP,
          secret: null,
        } as any;

        await expect(sut.buildRequest(verification)).rejects.toThrow(
          "Verification code is required",
        );
      });
    });

    describe("Master password verification", () => {
      beforeEach(() => {
        kdfConfigService.getKdfConfig.mockResolvedValue("kdfConfig" as unknown as KdfConfig);
        masterPasswordService.saltForUser$.mockReturnValue(of("salt" as any));
        masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue({
          masterPasswordAuthenticationHash: "hash",
        } as any);
      });

      it("should build request with master password secret", async () => {
        const verification = {
          type: VerificationType.MasterPassword,
          secret: "password123",
        } as any;

        const result = await sut.buildRequest(verification);

        expect(result.masterPasswordHash).toBe("hash");
      });

      it("should use default SecretVerificationRequest if no custom class provided", async () => {
        const verification = {
          type: VerificationType.MasterPassword,
          secret: "password123",
        } as any;

        const result = await sut.buildRequest(verification);

        expect(result).toHaveProperty("masterPasswordHash");
      });

      it("should get KDF config for the active user", async () => {
        const verification = {
          type: VerificationType.MasterPassword,
          secret: "password123",
        } as any;

        await sut.buildRequest(verification);

        expect(kdfConfigService.getKdfConfig).toHaveBeenCalledWith(mockUserId);
      });

      it("should get salt for the active user", async () => {
        const verification = {
          type: VerificationType.MasterPassword,
          secret: "password123",
        } as any;

        await sut.buildRequest(verification);

        expect(masterPasswordService.saltForUser$).toHaveBeenCalledWith(mockUserId);
      });

      it("should call makeMasterPasswordAuthenticationData with correct parameters", async () => {
        const verification = {
          type: VerificationType.MasterPassword,
          secret: "password123",
        } as any;

        await sut.buildRequest(verification);

        expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith(
          "password123",
          "kdfConfig",
          "salt",
        );
      });

      it("should throw if master password secret is empty", async () => {
        const verification = {
          type: VerificationType.MasterPassword,
          secret: "",
        } as any;

        await expect(sut.buildRequest(verification)).rejects.toThrow("Master Password is required");
      });

      it("should throw if master password secret is null", async () => {
        const verification = {
          type: VerificationType.MasterPassword,
          secret: null,
        } as any;

        await expect(sut.buildRequest(verification)).rejects.toThrow("Master Password is required");
      });
    });
  });

  describe("verifyUserByMasterPassword", () => {
    beforeAll(() => {
      i18nService.t.calledWith("invalidMasterPassword").mockReturnValue("Invalid master password");

      kdfConfigService.getKdfConfig.mockResolvedValue("kdfConfig" as unknown as KdfConfig);
      masterPasswordService.masterKey$.mockReturnValue(of("masterKey" as unknown as MasterKey));
      keyService.hashMasterKey
        .calledWith("password", "masterKey" as unknown as MasterKey, HashPurpose.LocalAuthorization)
        .mockResolvedValue("localHash");
    });

    describe("client-side verification", () => {
      beforeEach(() => {
        setMasterPasswordAvailability(true);
      });

      it("returns if verification is successful", async () => {
        keyService.compareKeyHash.mockResolvedValueOnce(true);

        const result = await sut.verifyUserByMasterPassword(
          {
            type: VerificationType.MasterPassword,
            secret: "password",
          } as MasterPasswordVerification,
          mockUserId,
          "email",
        );

        expect(keyService.compareKeyHash).toHaveBeenCalled();
        expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
          "localHash",
          mockUserId,
        );
        expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith("masterKey", mockUserId);
        expect(result).toEqual({
          policyOptions: null,
          masterKey: "masterKey",
          email: "email",
        });
      });

      it("throws if verification fails", async () => {
        keyService.compareKeyHash.mockResolvedValueOnce(false);

        await expect(
          sut.verifyUserByMasterPassword(
            {
              type: VerificationType.MasterPassword,
              secret: "password",
            } as MasterPasswordVerification,
            mockUserId,
            "email",
          ),
        ).rejects.toThrow("Invalid master password");

        expect(keyService.compareKeyHash).toHaveBeenCalled();
        expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith();
        expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith();
      });
    });

    describe("server-side verification", () => {
      beforeEach(() => {
        setMasterPasswordAvailability(false);
      });

      it("returns if verification is successful", async () => {
        keyService.hashMasterKey
          .calledWith(
            "password",
            "masterKey" as unknown as MasterKey,
            HashPurpose.ServerAuthorization,
          )
          .mockResolvedValueOnce("serverHash");
        userVerificationApiService.postAccountVerifyPassword.mockResolvedValueOnce(
          "MasterPasswordPolicyOptions" as unknown as MasterPasswordPolicyResponse,
        );

        const result = await sut.verifyUserByMasterPassword(
          {
            type: VerificationType.MasterPassword,
            secret: "password",
          } as MasterPasswordVerification,
          mockUserId,
          "email",
        );

        expect(keyService.compareKeyHash).not.toHaveBeenCalled();
        expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
          "localHash",
          mockUserId,
        );
        expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith("masterKey", mockUserId);
        expect(result).toEqual({
          policyOptions: "MasterPasswordPolicyOptions",
          masterKey: "masterKey",
          email: "email",
        });
      });

      it("throws if verification fails", async () => {
        keyService.hashMasterKey
          .calledWith(
            "password",
            "masterKey" as unknown as MasterKey,
            HashPurpose.ServerAuthorization,
          )
          .mockResolvedValueOnce("serverHash");
        userVerificationApiService.postAccountVerifyPassword.mockRejectedValueOnce(new Error());

        await expect(
          sut.verifyUserByMasterPassword(
            {
              type: VerificationType.MasterPassword,
              secret: "password",
            } as MasterPasswordVerification,
            mockUserId,
            "email",
          ),
        ).rejects.toThrow("Invalid master password");

        expect(keyService.compareKeyHash).not.toHaveBeenCalled();
        expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith();
        expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith();
      });
    });

    describe("error handling", () => {
      it("throws if any of the parameters are nullish", async () => {
        await expect(
          sut.verifyUserByMasterPassword(
            {
              type: VerificationType.MasterPassword,
              secret: null,
            } as MasterPasswordVerification,
            mockUserId,
            "email",
          ),
        ).rejects.toThrow(
          "Master Password is required. Cannot verify user without a master password.",
        );

        await expect(
          sut.verifyUserByMasterPassword(
            {
              type: VerificationType.MasterPassword,
              secret: "password",
            } as MasterPasswordVerification,
            null,
            "email",
          ),
        ).rejects.toThrow("User ID is required. Cannot verify user by master password.");

        await expect(
          sut.verifyUserByMasterPassword(
            {
              type: VerificationType.MasterPassword,
              secret: "password",
            } as MasterPasswordVerification,
            mockUserId,
            null,
          ),
        ).rejects.toThrow("Email is required. Cannot verify user by master password.");
      });

      it("throws if kdf config is not available", async () => {
        kdfConfigService.getKdfConfig.mockResolvedValueOnce(null);

        await expect(
          sut.verifyUserByMasterPassword(
            {
              type: VerificationType.MasterPassword,
              secret: "password",
            } as MasterPasswordVerification,
            mockUserId,
            "email",
          ),
        ).rejects.toThrow("KDF config is required. Cannot verify user by master password.");
      });

      it("throws if master key cannot be created", async () => {
        kdfConfigService.getKdfConfig.mockResolvedValueOnce("kdfConfig" as unknown as KdfConfig);
        masterPasswordService.masterKey$.mockReturnValueOnce(of(null));
        keyService.makeMasterKey.mockResolvedValueOnce(null);

        await expect(
          sut.verifyUserByMasterPassword(
            {
              type: VerificationType.MasterPassword,
              secret: "password",
            } as MasterPasswordVerification,
            mockUserId,
            "email",
          ),
        ).rejects.toThrow("Master key could not be created to verify the master password.");
      });
    });
  });

  // Helpers
  function setMasterPasswordAvailability(hasMasterPassword: boolean) {
    userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(hasMasterPassword));
    masterPasswordService.masterKeyHash$.mockReturnValue(
      of(hasMasterPassword ? "masterKeyHash" : null),
    );
  }

  function setPinAvailability(type: PinLockType) {
    pinService.getPinLockType.mockResolvedValue(type);

    if (type === "EPHEMERAL" || type === "PERSISTENT") {
      pinService.isPinDecryptionAvailable.mockResolvedValue(true);
    } else if (type === "DISABLED") {
      pinService.isPinDecryptionAvailable.mockResolvedValue(false);
    }
  }

  function disableBiometricsAvailability() {
    vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(false);
  }
});
