import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";

import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import {
  VaultTimeoutAction,
  VaultTimeoutSettingsService,
} from "@bitwarden/common/key-management/vault-timeout";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { makeEncString, FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";

import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";

import {
  AuthRequestLoginStrategy,
  AuthRequestLoginStrategyData,
} from "./auth-request-login.strategy";
import { identityTokenResponseFactory } from "./login.strategy.spec";

describe("AuthRequestLoginStrategy", () => {
  let cache: AuthRequestLoginStrategyData;

  let keyService: MockProxy<KeyService>;
  let encryptService: MockProxy<EncryptService>;
  let apiService: MockProxy<ApiService>;
  let tokenService: MockProxy<TokenService>;
  let appIdService: MockProxy<AppIdService>;
  let platformUtilsService: MockProxy<PlatformUtilsService>;
  let messagingService: MockProxy<MessagingService>;
  let logService: MockProxy<LogService>;
  let stateService: MockProxy<StateService>;
  let twoFactorService: MockProxy<TwoFactorService>;
  let userDecryptionOptions: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
  let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
  let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
  let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
  let kdfConfigService: MockProxy<KdfConfigService>;
  let environmentService: MockProxy<EnvironmentService>;
  let configService: MockProxy<ConfigService>;
  let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;

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

  let authRequestLoginStrategy: AuthRequestLoginStrategy;
  let credentials: AuthRequestLoginCredentials;
  let tokenResponse: IdentityTokenResponse;

  const deviceId = Utils.newGuid();

  const email = "EMAIL";
  const accessCode = "ACCESS_CODE";
  const authRequestId = "AUTH_REQUEST_ID";
  const decUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;

  beforeEach(async () => {
    cache = new AuthRequestLoginStrategyData();

    keyService = mock<KeyService>();
    encryptService = mock<EncryptService>();
    apiService = mock<ApiService>();
    tokenService = mock<TokenService>();
    appIdService = mock<AppIdService>();
    platformUtilsService = mock<PlatformUtilsService>();
    messagingService = mock<MessagingService>();
    logService = mock<LogService>();
    stateService = mock<StateService>();
    twoFactorService = mock<TwoFactorService>();
    userDecryptionOptions = mock<InternalUserDecryptionOptionsServiceAbstraction>();
    deviceTrustService = mock<DeviceTrustServiceAbstraction>();
    billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
    vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
    kdfConfigService = mock<KdfConfigService>();
    environmentService = mock<EnvironmentService>();
    configService = mock<ConfigService>();
    accountCryptographicStateService = mock<AccountCryptographicStateService>();

    accountService = mockAccountServiceWith(mockUserId);
    masterPasswordService = new FakeMasterPasswordService();

    tokenService.getTwoFactorToken.mockResolvedValue(null);
    appIdService.getAppId.mockResolvedValue(deviceId);
    tokenService.decodeAccessToken.mockResolvedValue({
      sub: mockUserId,
    });

    authRequestLoginStrategy = new AuthRequestLoginStrategy(
      cache,
      deviceTrustService,
      accountService,
      masterPasswordService,
      keyService,
      encryptService,
      apiService,
      tokenService,
      appIdService,
      platformUtilsService,
      messagingService,
      logService,
      stateService,
      twoFactorService,
      userDecryptionOptions,
      billingAccountProfileStateService,
      vaultTimeoutSettingsService,
      kdfConfigService,
      environmentService,
      configService,
      accountCryptographicStateService,
    );

    tokenResponse = identityTokenResponseFactory();
    apiService.postIdentityToken.mockResolvedValue(tokenResponse);

    const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
    const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
      mockVaultTimeoutAction,
    );
    vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
      mockVaultTimeoutActionBSub.asObservable(),
    );

    const mockVaultTimeout = 1000;

    const mockVaultTimeoutBSub = new BehaviorSubject<number>(mockVaultTimeout);
    vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
      mockVaultTimeoutBSub.asObservable(),
    );
  });

  it("sets keys after a successful authentication", async () => {
    // Initialize credentials with only userKey
    credentials = new AuthRequestLoginCredentials(
      email,
      accessCode,
      authRequestId,
      decUserKey, // Pass userKey
    );

    // Call logIn
    await authRequestLoginStrategy.logIn(credentials);

    // setMasterKey and setMasterKeyHash should not be called
    expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();

    // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
    expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
      tokenResponse.key,
      mockUserId,
    );
    expect(keyService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId);
    expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
      { V1: { private_key: tokenResponse.privateKey } },
      mockUserId,
    );

    // trustDeviceIfRequired should be called
    expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled();
  });

  it("sets account cryptographic state when accountKeysResponseModel is present", async () => {
    const accountKeysData = {
      publicKeyEncryptionKeyPair: {
        publicKey: "testPublicKey",
        wrappedPrivateKey: "testPrivateKey",
      },
    };

    tokenResponse = identityTokenResponseFactory();
    tokenResponse.key = makeEncString("mockEncryptedUserKey");
    // Add accountKeysResponseModel to the response
    (tokenResponse as any).accountKeysResponseModel = {
      publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair,
      toWrappedAccountCryptographicState: jest.fn().mockReturnValue({
        V1: {
          private_key: "testPrivateKey",
        },
      }),
    };

    apiService.postIdentityToken.mockResolvedValue(tokenResponse);
    masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(decUserKey);

    await authRequestLoginStrategy.logIn(credentials);

    expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1);
    expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
      {
        V1: {
          private_key: "testPrivateKey",
        },
      },
      mockUserId,
    );
  });
});
