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

import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { LogService } from "@bitwarden/logging";
import { UserId } from "@bitwarden/user-core";

import { DesktopAuthRequestAnsweringService } from "./desktop-auth-request-answering.service";

describe("DesktopAuthRequestAnsweringService", () => {
  let accountService: MockProxy<AccountService>;
  let authService: MockProxy<AuthService>;
  let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$
  let messagingService: MockProxy<MessagingService>;
  let pendingAuthRequestsState: MockProxy<PendingAuthRequestsStateService>;
  let i18nService: MockProxy<I18nService>;
  let logService: MockProxy<LogService>;
  let configService: MockProxy<ConfigService>;

  let sut: AuthRequestAnsweringService;

  const userId = "9f4c3452-6a45-48af-a7d0-74d3e8b65e4c" as UserId;
  const userAccountInfo = mockAccountInfoWith({
    name: "User",
    email: "user@example.com",
  });
  const userAccount: Account = {
    id: userId,
    ...userAccountInfo,
  };

  const authRequestId = "auth-request-id-123";

  beforeEach(() => {
    (global as any).ipc = {
      platform: {
        isWindowVisible: jest.fn(),
      },
      auth: {
        loginRequest: jest.fn(),
      },
    };

    accountService = mock<AccountService>();
    authService = mock<AuthService>();
    masterPasswordService = {
      forceSetPasswordReason$: jest.fn().mockReturnValue(of(ForceSetPasswordReason.None)),
    };
    messagingService = mock<MessagingService>();
    pendingAuthRequestsState = mock<PendingAuthRequestsStateService>();
    i18nService = mock<I18nService>();
    logService = mock<LogService>();
    configService = mock<ConfigService>();

    // Common defaults
    authService.activeAccountStatus$ = of(AuthenticationStatus.Locked);
    accountService.activeAccount$ = of(userAccount);
    accountService.accounts$ = of({
      [userId]: userAccountInfo,
    });
    (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false);
    i18nService.t.mockImplementation(
      (key: string, p1?: any) => `${key}${p1 != null ? ":" + p1 : ""}`,
    );
    configService.getFeatureFlag.mockResolvedValue(false);

    sut = new DesktopAuthRequestAnsweringService(
      accountService,
      authService,
      masterPasswordService,
      messagingService,
      pendingAuthRequestsState,
      i18nService,
      logService,
      configService,
    );
  });

  describe("receivedPendingAuthRequest()", () => {
    it("should throw if authRequestUserId not given", async () => {
      // Act
      const promise = sut.receivedPendingAuthRequest(undefined, undefined);

      // Assert
      await expect(promise).rejects.toThrow("authRequestUserId required");
    });

    it("should add a pending marker for the user to state", async () => {
      // Act
      await sut.receivedPendingAuthRequest(userId, authRequestId);

      // Assert
      expect(pendingAuthRequestsState.add).toHaveBeenCalledTimes(1);
      expect(pendingAuthRequestsState.add).toHaveBeenCalledWith(userId);
    });

    describe("given the active user is the intended recipient of the auth request, unlocked, and not required to set/change their master password", () => {
      describe("given the Desktop window is visible", () => {
        // TODO: PM-34438 - remove disabled tests once feature flag cleaned up
        describe("given the PM34210_DesktopAddDevices feature flag is disabled", () => {
          it("should send an 'openLoginApproval' message without notificationId", async () => {
            // Arrange
            (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
            authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
            configService.getFeatureFlag.mockImplementation(
              async (flag: FeatureFlag) => flag !== FeatureFlag.PM34210_DesktopAddDevices,
            );

            // Act
            await sut.receivedPendingAuthRequest(userId, authRequestId);

            // Assert
            expect(messagingService.send).toHaveBeenCalledTimes(1);
            expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval");
          });
        });

        describe("given the PM34210_DesktopAddDevices feature flag is enabled", () => {
          it("should send an 'openLoginApproval' message with notificationId", async () => {
            // Arrange
            (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
            authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
            configService.getFeatureFlag.mockImplementation(
              async (flag: FeatureFlag) => flag === FeatureFlag.PM34210_DesktopAddDevices,
            );

            // Act
            await sut.receivedPendingAuthRequest(userId, authRequestId);

            // Assert
            expect(messagingService.send).toHaveBeenCalledTimes(1);
            expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval", {
              notificationId: authRequestId,
            });
          });
        });

        it("should NOT create a system notification", async () => {
          // Arrange
          (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
          authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);

          // Act
          await sut.receivedPendingAuthRequest(userId, authRequestId);

          // Assert
          expect((global as any).ipc.auth.loginRequest).not.toHaveBeenCalled();
        });
      });

      describe("given the Desktop window is NOT visible", () => {
        it("should STILL send an 'openLoginApproval' message", async () => {
          // Arrange
          (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false);
          authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);

          // Act
          await sut.receivedPendingAuthRequest(userId, authRequestId);

          // Assert
          expect(messagingService.send).toHaveBeenCalledTimes(1);
          expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval");
        });

        it("should create a system notification", async () => {
          // Arrange
          (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false);
          authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);

          // Act
          await sut.receivedPendingAuthRequest(userId, authRequestId);

          // Assert
          expect(i18nService.t).toHaveBeenCalledWith("accountAccessRequested");
          expect(i18nService.t).toHaveBeenCalledWith("confirmAccessAttempt", "user@example.com");
          expect(i18nService.t).toHaveBeenCalledWith("close");

          expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith(
            "accountAccessRequested",
            "confirmAccessAttempt:user@example.com",
            "close",
          );
        });
      });
    });

    describe("given the active user is Locked", () => {
      it("should NOT send an 'openLoginApproval' message", async () => {
        // Arrange
        (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
        authService.activeAccountStatus$ = of(AuthenticationStatus.Locked);

        // Act
        await sut.receivedPendingAuthRequest(userId, authRequestId);

        // Assert
        expect(messagingService.send).not.toHaveBeenCalled();
      });

      it("should create a system notification", async () => {
        // Arrange
        (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
        authService.activeAccountStatus$ = of(AuthenticationStatus.Locked);

        // Act
        await sut.receivedPendingAuthRequest(userId, authRequestId);

        // Assert
        expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith(
          "accountAccessRequested",
          "confirmAccessAttempt:user@example.com",
          "close",
        );
      });
    });

    describe("given the active user is not the intended recipient of the auth request", () => {
      beforeEach(() => {
        // Different active user for these tests
        const differentUserId = "different-user-id" as UserId;
        accountService.activeAccount$ = of({
          id: differentUserId,
          ...mockAccountInfoWith({
            name: "Different User",
            email: "different@example.com",
          }),
        });
      });

      it("should NOT send an 'openLoginApproval' message", async () => {
        // Arrange
        (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
        authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);

        // Act
        // Pass in userId, not differentUserId (the active user), to mimic an auth
        // request coming in for a user who is not the active user
        await sut.receivedPendingAuthRequest(userId, authRequestId); // pass in userId, not differentUserId

        // Assert
        expect(messagingService.send).not.toHaveBeenCalled();
      });

      it("should create a system notification", async () => {
        // Arrange
        (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
        authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);

        // Act
        // Pass in userId, not differentUserId (the active user), to mimic an auth
        // request coming in for a user who is not the active user
        await sut.receivedPendingAuthRequest(userId, authRequestId);

        // Assert
        expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith(
          "accountAccessRequested",
          "confirmAccessAttempt:user@example.com",
          "close",
        );
      });
    });

    describe("given the active user is required to set/change their master password", () => {
      it("should NOT send an 'openLoginApproval' message", async () => {
        // Arrange
        (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
        authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
        masterPasswordService.forceSetPasswordReason$ = jest
          .fn()
          .mockReturnValue(of(ForceSetPasswordReason.WeakMasterPassword));

        // Act
        await sut.receivedPendingAuthRequest(userId, authRequestId);

        // Assert
        expect(messagingService.send).not.toHaveBeenCalled();
      });

      it("should create a system notification", async () => {
        // Arrange
        (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true);
        authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
        masterPasswordService.forceSetPasswordReason$ = jest
          .fn()
          .mockReturnValue(of(ForceSetPasswordReason.WeakMasterPassword));

        // Act
        await sut.receivedPendingAuthRequest(userId, authRequestId);

        // Assert
        expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith(
          "accountAccessRequested",
          "confirmAccessAttempt:user@example.com",
          "close",
        );
      });
    });
  });
});
