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

import { mockContainerService, mockEnc, mockFromJson } from "../../../../spec";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { LoginData } from "../../models/data/login.data";
import { Login } from "../../models/domain/login";
import { LoginUri } from "../../models/domain/login-uri";
import { LoginUriView } from "../../models/view/login-uri.view";
import { Fido2CredentialApi } from "../api/fido2-credential.api";
import { Fido2CredentialData } from "../data/fido2-credential.data";
import { Fido2CredentialView } from "../view/fido2-credential.view";

import { Fido2Credential } from "./fido2-credential";

describe("Login DTO", () => {
  beforeEach(() => {
    mockContainerService();
  });

  it("Convert from empty LoginData", () => {
    const data = new LoginData();
    const login = new Login(data);

    expect(login).toEqual({
      passwordRevisionDate: undefined,
      autofillOnPageLoad: undefined,
      username: undefined,
      password: undefined,
      totp: undefined,
    });

    expect(data.username).toBeUndefined();
    expect(data.password).toBeUndefined();
    expect(data.passwordRevisionDate).toBeUndefined();
    expect(data.totp).toBeUndefined();
    expect(data.autofillOnPageLoad).toBeUndefined();
    expect(data.uris).toBeUndefined();
    expect(data.fido2Credentials).toBeUndefined();
  });

  it("Convert from full LoginData", () => {
    const fido2CredentialData = initializeFido2Credential(new Fido2CredentialData());
    const data: LoginData = {
      uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
      username: "username",
      password: "password",
      passwordRevisionDate: "2022-01-31T12:00:00.000Z",
      totp: "123",
      autofillOnPageLoad: false,
      fido2Credentials: [fido2CredentialData],
    };
    const login = new Login(data);

    expect(login).toEqual({
      passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
      autofillOnPageLoad: false,
      username: { encryptedString: "username", encryptionType: 0 },
      password: { encryptedString: "password", encryptionType: 0 },
      totp: { encryptedString: "123", encryptionType: 0 },
      uris: [
        {
          match: 0,
          uri: { encryptedString: "uri", encryptionType: 0 },
          uriChecksum: { encryptedString: "checksum", encryptionType: 0 },
        },
      ],
      fido2Credentials: [encryptFido2Credential(fido2CredentialData)],
    });
  });

  it("Initialize without LoginData", () => {
    const login = new Login();

    expect(login).toEqual({});
  });

  describe("decrypt", () => {
    let loginUri: MockProxy<LoginUri>;
    const loginUriView = new LoginUriView();
    const decryptedFido2Credential = Symbol();
    const login = Object.assign(new Login(), {
      username: mockEnc("encrypted username"),
      password: mockEnc("encrypted password"),
      passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
      totp: mockEnc("encrypted totp"),
      autofillOnPageLoad: true,
      fido2Credentials: [{ decrypt: jest.fn().mockReturnValue(decryptedFido2Credential) } as any],
    });
    const expectedView = {
      username: "encrypted username",
      password: "encrypted password",
      passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
      totp: "encrypted totp",
      uris: [
        {
          _uri: "decrypted uri",
        },
      ],
      autofillOnPageLoad: true,
      fido2Credentials: [decryptedFido2Credential],
    };

    beforeEach(() => {
      loginUri = mock();
      loginUriView.uri = "decrypted uri";
    });

    it("should decrypt to a view", async () => {
      loginUri.decrypt.mockResolvedValue(loginUriView);
      loginUri.validateChecksum.mockResolvedValue(true);
      login.uris = [loginUri];

      const loginView = await login.decrypt(true, null);
      expect(loginView).toEqual(expectedView);
    });

    it("should ignore uris that fail checksum", async () => {
      loginUri.decrypt.mockResolvedValue(loginUriView);
      loginUri.validateChecksum
        .mockResolvedValueOnce(false)
        .mockResolvedValueOnce(false)
        .mockResolvedValueOnce(true);
      login.uris = [loginUri, loginUri, loginUri];

      const loginView = await login.decrypt(false, null);
      expect(loginView).toEqual(expectedView);
    });
  });

  it("Converts from LoginData and back", () => {
    const data: LoginData = {
      uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
      username: "username",
      password: "password",
      passwordRevisionDate: "2022-01-31T12:00:00.000Z",
      totp: "123",
      autofillOnPageLoad: false,
      fido2Credentials: [initializeFido2Credential(new Fido2CredentialData())],
    };
    const login = new Login(data);

    const loginData = login.toLoginData();

    expect(loginData).toEqual(data);
  });

  describe("fromJSON", () => {
    it("initializes nested objects", () => {
      jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
      jest.spyOn(LoginUri, "fromJSON").mockImplementation(mockFromJson);
      const passwordRevisionDate = new Date("2022-01-31T12:00:00.000Z");
      const fido2CreationDate = new Date("2023-01-01T12:00:00.000Z");

      const actual = Login.fromJSON({
        uris: ["loginUri1", "loginUri2"] as any,
        username: "myUsername" as EncryptedString,
        password: "myPassword" as EncryptedString,
        passwordRevisionDate: passwordRevisionDate.toISOString(),
        totp: "myTotp" as EncryptedString,
        // NOTE: `as any` is here until we migrate to Nx: https://bitwarden.atlassian.net/browse/PM-6493
        fido2Credentials: [
          {
            credentialId: "keyId" as EncryptedString,
            keyType: "keyType" as EncryptedString,
            keyAlgorithm: "keyAlgorithm" as EncryptedString,
            keyCurve: "keyCurve" as EncryptedString,
            keyValue: "keyValue" as EncryptedString,
            rpId: "rpId" as EncryptedString,
            userHandle: "userHandle" as EncryptedString,
            userName: "userName" as EncryptedString,
            counter: "counter" as EncryptedString,
            rpName: "rpName" as EncryptedString,
            userDisplayName: "userDisplayName" as EncryptedString,
            discoverable: "discoverable" as EncryptedString,
            creationDate: fido2CreationDate.toISOString(),
          },
        ] as any,
      });

      expect(actual).toEqual({
        uris: ["loginUri1_fromJSON", "loginUri2_fromJSON"] as any,
        username: "myUsername_fromJSON",
        password: "myPassword_fromJSON",
        passwordRevisionDate: passwordRevisionDate,
        totp: "myTotp_fromJSON",
        fido2Credentials: [
          {
            credentialId: "keyId_fromJSON",
            keyType: "keyType_fromJSON",
            keyAlgorithm: "keyAlgorithm_fromJSON",
            keyCurve: "keyCurve_fromJSON",
            keyValue: "keyValue_fromJSON",
            rpId: "rpId_fromJSON",
            userHandle: "userHandle_fromJSON",
            userName: "userName_fromJSON",
            counter: "counter_fromJSON",
            rpName: "rpName_fromJSON",
            userDisplayName: "userDisplayName_fromJSON",
            discoverable: "discoverable_fromJSON",
            creationDate: fido2CreationDate,
          },
        ],
      });
      expect(actual).toBeInstanceOf(Login);
    });

    it("returns undefined if object is null", () => {
      expect(Login.fromJSON(null)).toBeUndefined();
    });
  });

  describe("toSdkLogin", () => {
    it("should map to SDK login", () => {
      const data: LoginData = {
        uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
        username: "username",
        password: "password",
        passwordRevisionDate: "2022-01-31T12:00:00.000Z",
        totp: "123",
        autofillOnPageLoad: false,
        fido2Credentials: [initializeFido2Credential(new Fido2CredentialData())],
      };
      const login = new Login(data);
      const sdkLogin = login.toSdkLogin();

      expect(sdkLogin).toEqual({
        username: "username",
        password: "password",
        passwordRevisionDate: "2022-01-31T12:00:00.000Z",
        uris: [
          {
            match: 0,
            uri: "uri",
            uriChecksum: "checksum",
          },
        ],
        totp: "123",
        autofillOnPageLoad: false,
        fido2Credentials: [
          {
            credentialId: "credentialId",
            keyType: "public-key",
            keyAlgorithm: "ECDSA",
            keyCurve: "P-256",
            keyValue: "keyValue",
            rpId: "rpId",
            userHandle: "userHandle",
            userName: "userName",
            counter: "counter",
            rpName: "rpName",
            userDisplayName: "userDisplayName",
            discoverable: "discoverable",
            creationDate: "2023-01-01T12:00:00.000Z",
          },
        ],
      });
    });
  });
});

type Fido2CredentialLike = Fido2CredentialData | Fido2CredentialView | Fido2CredentialApi;
function initializeFido2Credential<T extends Fido2CredentialLike>(key: T): T {
  key.credentialId = "credentialId";
  key.keyType = "public-key";
  key.keyAlgorithm = "ECDSA";
  key.keyCurve = "P-256";
  key.keyValue = "keyValue";
  key.rpId = "rpId";
  key.userHandle = "userHandle";
  key.userName = "userName";
  key.counter = "counter";
  key.rpName = "rpName";
  key.userDisplayName = "userDisplayName";
  key.discoverable = "discoverable";
  key.creationDate = "2023-01-01T12:00:00.000Z";
  return key;
}

function encryptFido2Credential(key: Fido2CredentialLike): Fido2Credential {
  const encrypted = new Fido2Credential();
  encrypted.credentialId = { encryptedString: key.credentialId, encryptionType: 0 } as EncString;
  encrypted.keyType = { encryptedString: key.keyType, encryptionType: 0 } as EncString;
  encrypted.keyAlgorithm = { encryptedString: key.keyAlgorithm, encryptionType: 0 } as EncString;
  encrypted.keyCurve = { encryptedString: key.keyCurve, encryptionType: 0 } as EncString;
  encrypted.keyValue = { encryptedString: key.keyValue, encryptionType: 0 } as EncString;
  encrypted.rpId = { encryptedString: key.rpId, encryptionType: 0 } as EncString;
  encrypted.userHandle = { encryptedString: key.userHandle, encryptionType: 0 } as EncString;
  encrypted.userName = { encryptedString: key.userName, encryptionType: 0 } as EncString;
  encrypted.counter = { encryptedString: key.counter, encryptionType: 0 } as EncString;
  encrypted.rpName = { encryptedString: key.rpName, encryptionType: 0 } as EncString;
  encrypted.userDisplayName = {
    encryptedString: key.userDisplayName,
    encryptionType: 0,
  } as EncString;
  encrypted.discoverable = { encryptedString: key.discoverable, encryptionType: 0 } as EncString;

  // not encrypted
  encrypted.creationDate = new Date(key.creationDate);
  return encrypted;
}
