/**
 * include structuredClone in test environment.
 * @jest-environment ../../libs/shared/test.environment.ts
 */
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";

import { CipherListView } from "@bitwarden/sdk-internal";

import { ApiService } from "../../abstractions/api.service";
import { BillingAccountProfileStateService } from "../../billing/abstractions";
import { CipherId, UserId } from "../../types/guid";
import { CipherService } from "../abstractions/cipher.service";
import {
  CipherBulkArchiveRequest,
  CipherBulkUnarchiveRequest,
} from "../models/request/cipher-bulk-archive.request";

import { DefaultCipherArchiveService } from "./default-cipher-archive.service";

describe("DefaultCipherArchiveService", () => {
  let service: DefaultCipherArchiveService;
  let mockCipherService: jest.Mocked<CipherService>;
  let mockApiService: jest.Mocked<ApiService>;
  let mockBillingAccountProfileStateService: jest.Mocked<BillingAccountProfileStateService>;

  const userId = "user-id" as UserId;
  const cipherId = "123" as CipherId;

  beforeEach(() => {
    mockCipherService = mock<CipherService>();
    mockApiService = mock<ApiService>();
    mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();

    service = new DefaultCipherArchiveService(
      mockCipherService,
      mockApiService,
      mockBillingAccountProfileStateService,
    );
  });

  describe("archivedCiphers$", () => {
    it("should return only archived ciphers", async () => {
      const mockCiphers: CipherListView[] = [
        {
          id: "1",
          archivedDate: "2024-01-15T10:30:00.000Z",
          type: "identity",
        } as unknown as CipherListView,
        {
          id: "2",
          type: "secureNote",
        } as unknown as CipherListView,
        {
          id: "3",
          archivedDate: "2024-01-15T10:30:00.000Z",
          deletedDate: "2024-01-16T10:30:00.000Z",
          type: "sshKey",
        } as unknown as CipherListView,
      ];

      mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));

      const result = await firstValueFrom(service.archivedCiphers$(userId));

      expect(result).toHaveLength(1);
      expect(result[0].id).toEqual("1");
    });

    it("should return empty array when no archived ciphers exist", async () => {
      const mockCiphers: CipherListView[] = [
        {
          id: "1",
          type: "identity",
        } as unknown as CipherListView,
      ];

      mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));

      const result = await firstValueFrom(service.archivedCiphers$(userId));

      expect(result).toHaveLength(0);
    });
  });

  describe("userCanArchive$", () => {
    it("should return true when user has premium", async () => {
      mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));

      const result = await firstValueFrom(service.userCanArchive$(userId));

      expect(result).toBe(true);
      expect(mockBillingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
        userId,
      );
    });

    it("should return false when user does not have premium", async () => {
      mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));

      const result = await firstValueFrom(service.userCanArchive$(userId));

      expect(result).toBe(false);
    });
  });

  describe("userHasPremium$", () => {
    it("returns true when user has premium", async () => {
      mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));

      const result = await firstValueFrom(service.userHasPremium$(userId));

      expect(result).toBe(true);
      expect(mockBillingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
        userId,
      );
    });

    it("returns false when user does not have premium", async () => {
      mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));

      const result = await firstValueFrom(service.userHasPremium$(userId));

      expect(result).toBe(false);
    });
  });

  describe("showSubscriptionEndedMessaging$", () => {
    it("returns true when user has archived ciphers but no premium", async () => {
      const mockCiphers: CipherListView[] = [
        {
          id: "1",
          archivedDate: "2024-01-15T10:30:00.000Z",
          type: "identity",
        } as unknown as CipherListView,
      ];

      mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));
      mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));

      const result = await firstValueFrom(service.showSubscriptionEndedMessaging$(userId));

      expect(result).toBe(true);
    });

    it("returns false when user has archived ciphers and has premium", async () => {
      const mockCiphers: CipherListView[] = [
        {
          id: "1",
          archivedDate: "2024-01-15T10:30:00.000Z",
          type: "identity",
        } as unknown as CipherListView,
      ];

      mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));
      mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));

      const result = await firstValueFrom(service.showSubscriptionEndedMessaging$(userId));

      expect(result).toBe(false);
    });

    it("returns false when user has no archived ciphers and no premium", async () => {
      mockCipherService.cipherListViews$.mockReturnValue(of([]));
      mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));

      const result = await firstValueFrom(service.showSubscriptionEndedMessaging$(userId));

      expect(result).toBe(false);
    });
  });

  describe("archiveWithServer", () => {
    const mockResponse = {
      data: [
        {
          id: cipherId,
          archivedDate: "2024-01-15T10:30:00.000Z",
          revisionDate: "2024-01-15T10:31:00.000Z",
        },
      ],
    };

    beforeEach(() => {
      mockApiService.send.mockResolvedValue(mockResponse);
      mockCipherService.ciphers$.mockReturnValue(
        of({
          [cipherId]: {
            id: cipherId,
            revisionDate: "2024-01-15T10:00:00.000Z",
          } as any,
        }),
      );
      mockCipherService.upsert.mockResolvedValue(undefined);
    });

    it("should archive single cipher", async () => {
      await service.archiveWithServer(cipherId, userId);

      expect(mockApiService.send).toHaveBeenCalledWith(
        "PUT",
        "/ciphers/archive",
        expect.any(CipherBulkArchiveRequest),
        true,
        true,
      );
      expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
      expect(mockCipherService.upsert).toHaveBeenCalledWith(
        [
          expect.objectContaining({
            archivedDate: "2024-01-15T10:30:00.000Z",
            revisionDate: "2024-01-15T10:31:00.000Z",
          }),
        ],
        userId,
      );
    });

    it("should archive multiple ciphers", async () => {
      const cipherIds = [cipherId, "cipher-id-2" as CipherId];

      await service.archiveWithServer(cipherIds, userId);

      expect(mockApiService.send).toHaveBeenCalledWith(
        "PUT",
        "/ciphers/archive",
        expect.objectContaining({
          ids: cipherIds,
        }),
        true,
        true,
      );
    });
  });

  describe("unarchiveWithServer", () => {
    const mockResponse = {
      data: [
        {
          id: cipherId,
          revisionDate: "2024-01-15T10:31:00.000Z",
        },
      ],
    };

    beforeEach(() => {
      mockApiService.send.mockResolvedValue(mockResponse);
      mockCipherService.ciphers$.mockReturnValue(
        of({
          [cipherId]: {
            id: cipherId,
            archivedDate: "2024-01-15T10:30:00.000Z",
            revisionDate: "2024-01-15T10:00:00.000Z",
          } as any,
        }),
      );
      mockCipherService.upsert.mockResolvedValue(undefined);
    });

    it("should unarchive single cipher", async () => {
      await service.unarchiveWithServer(cipherId, userId);

      expect(mockApiService.send).toHaveBeenCalledWith(
        "PUT",
        "/ciphers/unarchive",
        expect.any(CipherBulkUnarchiveRequest),
        true,
        true,
      );
      expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
      expect(mockCipherService.upsert).toHaveBeenCalledWith(
        [
          expect.objectContaining({
            revisionDate: "2024-01-15T10:31:00.000Z",
          }),
        ],
        userId,
      );
    });

    it("should unarchive multiple ciphers", async () => {
      const cipherIds = [cipherId, "cipher-id-2" as CipherId];

      await service.unarchiveWithServer(cipherIds, userId);

      expect(mockApiService.send).toHaveBeenCalledWith(
        "PUT",
        "/ciphers/unarchive",
        expect.objectContaining({
          ids: cipherIds,
        }),
        true,
        true,
      );
    });
  });
});
