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

import {
  CipherView as SdkCipherView,
  CreateAttachmentRequest,
  EncString,
} from "@bitwarden/sdk-internal";

import { LogService } from "../../platform/abstractions/log.service";
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
import {
  UserId,
  CipherId,
  EmergencyAccessId,
  OrganizationId,
  CollectionId,
} from "../../types/guid";
import { CipherType } from "../enums/cipher-type";
import { Cipher } from "../models/domain/cipher";
import { CipherView } from "../models/view/cipher.view";
import { Fido2CredentialView } from "../models/view/fido2-credential.view";

import { DefaultCipherSdkService } from "./cipher-sdk.service";

describe("DefaultCipherSdkService", () => {
  const sdkService = mock<SdkService>();
  const logService = mock<LogService>();
  const userId = "test-user-id" as UserId;
  const cipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
  const orgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b21" as OrganizationId;

  let cipherSdkService: DefaultCipherSdkService;
  let mockSdkClient: any;
  let mockCiphersSdk: any;
  let mockAdminSdk: any;
  let mockAttachmentsSdk: any;
  let mockAttachmentsAdminSdk: any;
  let mockVaultSdk: any;

  beforeEach(() => {
    mockAdminSdk = {
      create: jest.fn(),
      edit: jest.fn(),
      delete: jest.fn().mockResolvedValue(undefined),
      delete_many: jest.fn().mockResolvedValue(undefined),
      soft_delete: jest.fn().mockResolvedValue(undefined),
      soft_delete_many: jest.fn().mockResolvedValue(undefined),
      restore: jest.fn().mockResolvedValue(undefined),
      restore_many: jest.fn().mockResolvedValue(undefined),
      list_org_ciphers: jest.fn().mockResolvedValue({ ciphers: [], listViews: [] }),
      list_assigned_org_ciphers: jest.fn().mockResolvedValue({ ciphers: [], listViews: [] }),
      update_collection: jest.fn(),
      delete_attachment: jest.fn().mockResolvedValue(undefined),
    };
    mockCiphersSdk = {
      create: jest.fn(),
      edit: jest.fn(),
      edit_partial: jest.fn(),
      delete: jest.fn().mockResolvedValue(undefined),
      delete_many: jest.fn().mockResolvedValue(undefined),
      soft_delete: jest.fn().mockResolvedValue(undefined),
      soft_delete_many: jest.fn().mockResolvedValue(undefined),
      restore: jest.fn().mockResolvedValue(undefined),
      restore_many: jest.fn().mockResolvedValue(undefined),
      share_cipher: jest.fn(),
      share_ciphers_bulk: jest.fn(),
      decrypt_fido2_credentials: jest.fn(),
      decrypt_fido2_private_key: jest.fn(),
      get_all: jest.fn().mockResolvedValue({ successes: [], failures: [] }),
      update_collection: jest.fn(),
      delete_attachment: jest.fn(),
      bulk_update_collections: jest.fn().mockResolvedValue(undefined),
      move_many: jest.fn().mockResolvedValue(undefined),
      admin: jest.fn().mockReturnValue(mockAdminSdk),
    };
    mockAttachmentsAdminSdk = {
      delete_attachment: jest.fn().mockResolvedValue(undefined),
      get_attachment_download_url: jest.fn(),
    };
    mockAttachmentsSdk = {
      create_attachment: jest.fn(),
      delete_attachment: jest.fn(),
      get_attachment_download_url: jest.fn(),
      upgrade_attachment: jest.fn(),
      renew_file_upload_url: jest.fn(),
      admin: jest.fn().mockReturnValue(mockAttachmentsAdminSdk),
    };
    mockVaultSdk = {
      ciphers: jest.fn().mockReturnValue(mockCiphersSdk),
      attachments: jest.fn().mockReturnValue(mockAttachmentsSdk),
    };
    const mockSdkValue = {
      vault: jest.fn().mockReturnValue(mockVaultSdk),
    };
    mockSdkClient = {
      take: jest.fn().mockReturnValue({
        value: mockSdkValue,
        [Symbol.dispose]: jest.fn(),
      }),
    };

    // Mock sdkService to return the mock client
    sdkService.userClient$.mockReturnValue(of(mockSdkClient));

    cipherSdkService = new DefaultCipherSdkService(sdkService, logService);
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  const createMockSdkCipherView = (id: string, collectionIds: CollectionId[] = []): any => ({
    id,
    organizationId: orgId,
    folderId: null,
    collectionIds,
    key: null,
    name: "EncryptedString",
    notes: null,
    type: CipherType.Login,
    login: null,
    identity: null,
    card: null,
    secureNote: null,
    sshKey: null,
    data: null,
    favorite: false,
    reprompt: 0,
    organizationUseTotp: false,
    edit: true,
    permissions: null,
    viewPassword: true,
    localData: null,
    attachments: null,
    fields: null,
    passwordHistory: null,
    creationDate: "2022-01-01T12:00:00.000Z",
    deletedDate: null,
    archivedDate: null,
    revisionDate: "2022-01-31T12:00:00.000Z",
  });

  describe("createWithServer()", () => {
    it("should create cipher using SDK when orgAdmin is false", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Test Cipher";
      cipherView.organizationId = orgId;
      cipherView.archivedDate = new Date("2024-01-01T12:00:00.000Z");

      const mockSdkCipherView = cipherView.toSdkCipherView();
      mockCiphersSdk.create.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.createWithServer(cipherView, userId, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.create).toHaveBeenCalledWith(
        expect.objectContaining({
          name: cipherView.name,
          organizationId: expect.anything(),
          archivedDate: "2024-01-01T12:00:00.000Z",
        }),
      );
      expect(result).toBeInstanceOf(CipherView);
      expect(result?.name).toBe(cipherView.name);
    });

    it("should create cipher using SDK admin API when orgAdmin is true", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Test Admin Cipher";
      cipherView.organizationId = orgId;

      const mockSdkCipherView = cipherView.toSdkCipherView();
      mockAdminSdk.create.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.createWithServer(cipherView, userId, true);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.create).toHaveBeenCalledWith(
        expect.objectContaining({
          name: cipherView.name,
        }),
      );
      expect(result).toBeInstanceOf(CipherView);
      expect(result?.name).toBe(cipherView.name);
    });

    it("should decrypt FIDO2 credentials from create response", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Test Cipher";
      cipherView.organizationId = orgId;

      // Build an SDK response that includes encrypted FIDO2 credentials
      const mockSdkResponse = {
        ...cipherView.toSdkCipherView(),
        login: {
          ...cipherView.toSdkCipherView().login,
          fido2Credentials: [{ credentialId: "encrypted-cred-id" }],
        },
      } as unknown as SdkCipherView;
      mockCiphersSdk.create.mockResolvedValue(mockSdkResponse);

      // Mock FIDO2 decryption
      const mockDecryptedFido2 = [{ credentialId: "decrypted-cred-id" }];
      mockCiphersSdk.decrypt_fido2_credentials.mockReturnValue(mockDecryptedFido2);
      mockCiphersSdk.decrypt_fido2_private_key.mockReturnValue("decrypted-key-value");

      const mockFido2View = new Fido2CredentialView();
      mockFido2View.credentialId = "decrypted-cred-id";
      jest.spyOn(Fido2CredentialView, "fromSdkFido2CredentialView").mockReturnValue(mockFido2View);

      const result = await cipherSdkService.createWithServer(cipherView, userId, false);

      expect(mockCiphersSdk.decrypt_fido2_credentials).toHaveBeenCalledWith(mockSdkResponse);
      expect(mockCiphersSdk.decrypt_fido2_private_key).toHaveBeenCalledWith(mockSdkResponse);
      expect(result?.login?.fido2Credentials).toHaveLength(1);
      expect(result?.login?.fido2Credentials?.[0].credentialId).toBe("decrypted-cred-id");
      expect(result?.login?.fido2Credentials?.[0].keyValue).toBe("decrypted-key-value");
    });

    it("should throw error and log when SDK throws an error", async () => {
      const cipherView = new CipherView();
      cipherView.name = "Test Cipher";

      mockCiphersSdk.create.mockRejectedValue(new Error("SDK error"));

      await expect(cipherSdkService.createWithServer(cipherView, userId)).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to create cipher"),
      );
    });
  });

  describe("updateWithServer()", () => {
    it("should update cipher using SDK edit when orgAdmin is false and cipher.edit is true", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Updated Cipher";
      cipherView.organizationId = orgId;
      cipherView.edit = true;

      const mockSdkCipherView = cipherView.toSdkCipherView();
      mockCiphersSdk.edit.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.updateWithServer(cipherView, userId, undefined, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.edit).toHaveBeenCalledWith(
        expect.objectContaining({
          id: expect.anything(),
          name: cipherView.name,
        }),
      );
      expect(mockCiphersSdk.edit_partial).not.toHaveBeenCalled();
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
      expect(result).toBeInstanceOf(CipherView);
      expect(result.name).toBe(cipherView.name);
    });

    it("should partial update cipher using SDK edit_partial when orgAdmin is false and cipher.edit is false", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "View-Only Cipher";
      cipherView.organizationId = orgId;
      cipherView.edit = false;
      cipherView.favorite = true;

      const mockSdkCipherView = cipherView.toSdkCipherView();
      mockCiphersSdk.edit_partial.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.updateWithServer(cipherView, userId, undefined, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.edit_partial).toHaveBeenCalledWith(
        expect.objectContaining({
          id: expect.anything(),
          favorite: true,
        }),
      );
      expect(mockCiphersSdk.edit).not.toHaveBeenCalled();
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
      expect(result).toBeInstanceOf(CipherView);
    });

    it("should partial update cipher when orgAdmin is false and cipher.edit defaults to false", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Default Edit Cipher";
      cipherView.organizationId = orgId;

      const mockSdkCipherView = cipherView.toSdkCipherView();
      mockCiphersSdk.edit_partial.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.updateWithServer(cipherView, userId, undefined, false);

      expect(mockCiphersSdk.edit_partial).toHaveBeenCalled();
      expect(mockCiphersSdk.edit).not.toHaveBeenCalled();
      expect(result).toBeInstanceOf(CipherView);
    });

    it("should update cipher using SDK admin API when orgAdmin is true", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Updated Admin Cipher";
      cipherView.organizationId = orgId;

      const originalCipherView = new CipherView();
      originalCipherView.id = cipherId;
      originalCipherView.name = "Original Cipher";

      const mockSdkCipherView = cipherView.toSdkCipherView();
      mockAdminSdk.edit.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.updateWithServer(
        cipherView,
        userId,
        originalCipherView,
        true,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.edit).toHaveBeenCalledWith(
        expect.objectContaining({
          id: expect.anything(),
          name: cipherView.name,
        }),
        originalCipherView.toSdkCipherView(),
      );
      expect(result).toBeInstanceOf(CipherView);
      expect(result.name).toBe(cipherView.name);
    });

    it("should update cipher using SDK admin API without originalCipherView", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Updated Admin Cipher";
      cipherView.organizationId = orgId;

      const mockSdkCipherView = cipherView.toSdkCipherView();
      mockAdminSdk.edit.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.updateWithServer(cipherView, userId, undefined, true);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.edit).toHaveBeenCalledWith(
        expect.objectContaining({
          id: expect.anything(),
          name: cipherView.name,
        }),
        expect.anything(), // Empty CipherView - timestamps vary so we just verify it was called
      );
      expect(result).toBeInstanceOf(CipherView);
      expect(result.name).toBe(cipherView.name);
    });

    it("should decrypt FIDO2 credentials from edit response", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Updated Cipher";
      cipherView.organizationId = orgId;
      cipherView.edit = true;

      // Build an SDK response that includes encrypted FIDO2 credentials
      const mockSdkResponse = {
        ...cipherView.toSdkCipherView(),
        login: {
          ...cipherView.toSdkCipherView().login,
          fido2Credentials: [{ credentialId: "encrypted-cred-id" }],
        },
      } as unknown as SdkCipherView;
      mockCiphersSdk.edit.mockResolvedValue(mockSdkResponse);

      // Mock FIDO2 decryption
      const mockDecryptedFido2 = [{ credentialId: "decrypted-cred-id" }];
      mockCiphersSdk.decrypt_fido2_credentials.mockReturnValue(mockDecryptedFido2);
      mockCiphersSdk.decrypt_fido2_private_key.mockReturnValue("decrypted-key-value");

      const mockFido2View = new Fido2CredentialView();
      mockFido2View.credentialId = "decrypted-cred-id";
      jest.spyOn(Fido2CredentialView, "fromSdkFido2CredentialView").mockReturnValue(mockFido2View);

      const result = await cipherSdkService.updateWithServer(cipherView, userId, undefined, false);

      expect(mockCiphersSdk.decrypt_fido2_credentials).toHaveBeenCalledWith(mockSdkResponse);
      expect(mockCiphersSdk.decrypt_fido2_private_key).toHaveBeenCalledWith(mockSdkResponse);
      expect(result?.login?.fido2Credentials).toHaveLength(1);
      expect(result?.login?.fido2Credentials?.[0].credentialId).toBe("decrypted-cred-id");
      expect(result?.login?.fido2Credentials?.[0].keyValue).toBe("decrypted-key-value");
    });

    it("should throw error and log when SDK throws an error", async () => {
      const cipherView = new CipherView();
      cipherView.name = "Test Cipher";
      cipherView.edit = true;

      mockCiphersSdk.edit.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.updateWithServer(cipherView, userId, undefined, false),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to update cipher"),
      );
    });

    it("should throw error and log when SDK edit_partial throws an error", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.name = "Test Cipher";
      cipherView.edit = false;

      mockCiphersSdk.edit_partial.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.updateWithServer(cipherView, userId, undefined, false),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to update cipher"),
      );
    });
  });

  describe("deleteWithServer()", () => {
    const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;

    it("should delete cipher using SDK when asAdmin is false", async () => {
      await cipherSdkService.deleteWithServer(testCipherId, userId, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.delete).toHaveBeenCalledWith(testCipherId);
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
    });

    it("should delete cipher using SDK admin API when asAdmin is true", async () => {
      await cipherSdkService.deleteWithServer(testCipherId, userId, true);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.delete).toHaveBeenCalledWith(testCipherId);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.delete.mockRejectedValue(new Error("SDK error"));

      await expect(cipherSdkService.deleteWithServer(testCipherId, userId)).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to delete cipher"),
      );
    });
  });

  describe("deleteManyWithServer()", () => {
    const testCipherIds = [
      "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
      "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
    ];

    it("should delete multiple ciphers using SDK when asAdmin is false", async () => {
      await cipherSdkService.deleteManyWithServer(testCipherIds, userId, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.delete_many).toHaveBeenCalledWith(testCipherIds);
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
    });

    it("should delete multiple ciphers using SDK admin API when asAdmin is true", async () => {
      await cipherSdkService.deleteManyWithServer(testCipherIds, userId, true, orgId);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
    });

    it("should throw error when asAdmin is true but orgId is missing", async () => {
      await expect(
        cipherSdkService.deleteManyWithServer(testCipherIds, userId, true, undefined),
      ).rejects.toThrow("Organization ID is required for admin delete.");
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.delete_many.mockRejectedValue(new Error("SDK error"));

      await expect(cipherSdkService.deleteManyWithServer(testCipherIds, userId)).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to delete multiple ciphers"),
      );
    });
  });

  describe("softDeleteWithServer()", () => {
    const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;

    it("should soft delete cipher using SDK when asAdmin is false", async () => {
      await cipherSdkService.softDeleteWithServer(testCipherId, userId, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
    });

    it("should soft delete cipher using SDK admin API when asAdmin is true", async () => {
      await cipherSdkService.softDeleteWithServer(testCipherId, userId, true);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.soft_delete.mockRejectedValue(new Error("SDK error"));

      await expect(cipherSdkService.softDeleteWithServer(testCipherId, userId)).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to soft delete cipher"),
      );
    });
  });

  describe("softDeleteManyWithServer()", () => {
    const testCipherIds = [
      "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
      "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
    ];

    it("should soft delete multiple ciphers using SDK when asAdmin is false", async () => {
      await cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds);
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
    });

    it("should soft delete multiple ciphers using SDK admin API when asAdmin is true", async () => {
      await cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, true, orgId);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
    });

    it("should throw error when asAdmin is true but orgId is missing", async () => {
      await expect(
        cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, true, undefined),
      ).rejects.toThrow("Organization ID is required for admin soft delete.");
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.soft_delete_many.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.softDeleteManyWithServer(testCipherIds, userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to soft delete multiple ciphers"),
      );
    });
  });

  describe("restoreWithServer()", () => {
    const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;

    it("should restore cipher using SDK when asAdmin is false", async () => {
      await cipherSdkService.restoreWithServer(testCipherId, userId, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.restore).toHaveBeenCalledWith(testCipherId);
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
    });

    it("should restore cipher using SDK admin API when asAdmin is true", async () => {
      await cipherSdkService.restoreWithServer(testCipherId, userId, true);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.restore).toHaveBeenCalledWith(testCipherId);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.restore.mockRejectedValue(new Error("SDK error"));

      await expect(cipherSdkService.restoreWithServer(testCipherId, userId)).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to restore cipher"),
      );
    });
  });

  describe("restoreManyWithServer()", () => {
    const testCipherIds = [
      "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
      "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
    ];

    it("should restore multiple ciphers using SDK when orgId is not provided", async () => {
      await cipherSdkService.restoreManyWithServer(testCipherIds, userId);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.restore_many).toHaveBeenCalledWith(testCipherIds);
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
    });

    it("should restore multiple ciphers using SDK admin API when orgId is provided", async () => {
      const orgIdString = orgId as string;
      await cipherSdkService.restoreManyWithServer(testCipherIds, userId, orgIdString);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.restore_many).toHaveBeenCalledWith(testCipherIds, orgIdString);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.restore_many.mockRejectedValue(new Error("SDK error"));

      await expect(cipherSdkService.restoreManyWithServer(testCipherIds, userId)).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to restore multiple ciphers"),
      );
    });
  });

  describe("shareWithServer()", () => {
    const collectionId1 = "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CollectionId;
    const collectionId2 = "7ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b24" as CollectionId;

    it("should share cipher using SDK", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Test Cipher";

      const mockSdkCipher = createMockSdkCipherView(cipherId);
      mockCiphersSdk.share_cipher.mockResolvedValue(mockSdkCipher);

      const result = await cipherSdkService.shareWithServer(
        cipherView,
        orgId,
        [collectionId1, collectionId2],
        userId,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.share_cipher).toHaveBeenCalledWith(
        expect.objectContaining({
          name: cipherView.name,
        }),
        orgId,
        [collectionId1, collectionId2],
        undefined,
      );
      expect(result).toBeInstanceOf(CipherView);
    });

    it("should pass originalCipherView to SDK when provided", async () => {
      const cipherView = new CipherView();
      cipherView.id = cipherId;
      cipherView.type = CipherType.Login;
      cipherView.name = "Test Cipher";

      const originalCipherView = new CipherView();
      originalCipherView.id = cipherId;
      originalCipherView.name = "Original Cipher";

      const mockSdkCipher = createMockSdkCipherView(cipherId);
      mockCiphersSdk.share_cipher.mockResolvedValue(mockSdkCipher);

      await cipherSdkService.shareWithServer(
        cipherView,
        orgId,
        [collectionId1],
        userId,
        originalCipherView,
      );

      expect(mockCiphersSdk.share_cipher).toHaveBeenCalledWith(
        expect.anything(),
        orgId,
        [collectionId1],
        expect.objectContaining({ name: "Original Cipher" }),
      );
    });

    it("should throw error and log when SDK throws an error", async () => {
      const cipherView = new CipherView();
      cipherView.name = "Test Cipher";

      mockCiphersSdk.share_cipher.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.shareWithServer(cipherView, orgId, [collectionId1], userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to share cipher"),
      );
    });
  });

  describe("shareManyWithServer()", () => {
    const collectionId1 = "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CollectionId;
    const cipherId2 = "8ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b25" as CipherId;

    it("should share multiple ciphers using SDK", async () => {
      const cipherView1 = new CipherView();
      cipherView1.id = cipherId;
      cipherView1.type = CipherType.Login;
      cipherView1.name = "Test Cipher 1";

      const cipherView2 = new CipherView();
      cipherView2.id = cipherId2;
      cipherView2.type = CipherType.Login;
      cipherView2.name = "Test Cipher 2";

      const mockSdkCiphers = [
        createMockSdkCipherView(cipherId),
        createMockSdkCipherView(cipherId2),
      ];
      mockCiphersSdk.share_ciphers_bulk.mockResolvedValue(mockSdkCiphers);

      const result = await cipherSdkService.shareManyWithServer(
        [cipherView1, cipherView2],
        orgId,
        [collectionId1],
        userId,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.share_ciphers_bulk).toHaveBeenCalledWith(
        expect.arrayContaining([
          expect.objectContaining({ name: cipherView1.name }),
          expect.objectContaining({ name: cipherView2.name }),
        ]),
        orgId,
        [collectionId1],
      );
      expect(result).toHaveLength(2);
      expect(result[0]).toBeInstanceOf(CipherView);
      expect(result[1]).toBeInstanceOf(CipherView);
    });

    it("should throw error and log when SDK throws an error", async () => {
      const cipherView = new CipherView();
      cipherView.name = "Test Cipher";

      mockCiphersSdk.share_ciphers_bulk.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.shareManyWithServer([cipherView], orgId, [collectionId1], userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to share multiple ciphers"),
      );
    });
  });

  describe("deleteAttachmentWithServer()", () => {
    const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
    const testAttachmentId = "uf7bkexzag04d3cw04jsbqqkbpbwhxs0";

    const createMockSdkCipher = (id: string): any => ({
      id,
      name: "2.encryptedName|iv|data",
      type: CipherType.Login,
      organizationId: null,
      folderId: null,
      favorite: false,
      edit: true,
      viewPassword: true,
      organizationUseTotp: false,
      revisionDate: "2026-04-23T12:00:00.000Z",
      creationDate: "2022-01-01T12:00:00.000Z",
      collectionIds: [],
      deletedDate: null,
      reprompt: 0,
      key: null,
      localData: null,
      attachments: null,
      fields: null,
      passwordHistory: null,
      notes: null,
      login: null,
      secureNote: null,
      card: null,
      identity: null,
      sshKey: null,
      permissions: null,
    });

    it("should delete attachment using SDK and return mapped cipher when asAdmin is false", async () => {
      const mockSdkCipher = createMockSdkCipher(testCipherId);
      mockAttachmentsSdk.delete_attachment.mockResolvedValue(mockSdkCipher);

      const result = await cipherSdkService.deleteAttachmentWithServer(
        testCipherId,
        testAttachmentId,
        userId,
        false,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.attachments).toHaveBeenCalled();
      expect(mockAttachmentsSdk.delete_attachment).toHaveBeenCalledWith(
        testCipherId,
        testAttachmentId,
      );
      expect(mockAttachmentsSdk.admin).not.toHaveBeenCalled();
      expect(result).toBeInstanceOf(Cipher);
      expect(result?.id).toBe(testCipherId);
    });

    it("should delete attachment using SDK admin API and return mapped cipher when asAdmin is true", async () => {
      const mockSdkCipher = createMockSdkCipher(testCipherId);
      mockAttachmentsAdminSdk.delete_attachment.mockResolvedValue(mockSdkCipher);

      const result = await cipherSdkService.deleteAttachmentWithServer(
        testCipherId,
        testAttachmentId,
        userId,
        true,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.attachments).toHaveBeenCalled();
      expect(mockAttachmentsSdk.admin).toHaveBeenCalled();
      expect(mockAttachmentsAdminSdk.delete_attachment).toHaveBeenCalledWith(
        testCipherId,
        testAttachmentId,
      );
      expect(mockAttachmentsSdk.delete_attachment).not.toHaveBeenCalled();
      expect(result).toBeInstanceOf(Cipher);
      expect(result?.id).toBe(testCipherId);
    });

    it("should throw error and log when SDK throws an error on user path", async () => {
      mockAttachmentsSdk.delete_attachment.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.deleteAttachmentWithServer(testCipherId, testAttachmentId, userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to delete cipher attachment"),
      );
    });

    it("should throw error and log when SDK throws an error on admin path", async () => {
      mockAttachmentsAdminSdk.delete_attachment.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.deleteAttachmentWithServer(testCipherId, testAttachmentId, userId, true),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to delete cipher attachment"),
      );
    });
  });

  describe("getAttachmentDownloadUrl()", () => {
    const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
    const testAttachmentId = "uf7bkexzag04d3cw04jsbqqkbpbwhxs0";
    const testEmergencyAccessId = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" as EmergencyAccessId;
    const expectedUrl = "https://example.com/attachment/test";

    it("calls the regular SDK method when no options are provided", async () => {
      mockAttachmentsSdk.get_attachment_download_url.mockResolvedValue(expectedUrl);

      const url = await cipherSdkService.getAttachmentDownloadUrl(
        testCipherId,
        testAttachmentId,
        userId,
      );

      expect(mockAttachmentsSdk.get_attachment_download_url).toHaveBeenCalledWith(
        testCipherId,
        testAttachmentId,
        undefined,
      );
      expect(mockAttachmentsAdminSdk.get_attachment_download_url).not.toHaveBeenCalled();
      expect(url).toBe(expectedUrl);
    });

    it("calls the admin SDK method when asAdmin is true", async () => {
      mockAttachmentsAdminSdk.get_attachment_download_url.mockResolvedValue(expectedUrl);

      const url = await cipherSdkService.getAttachmentDownloadUrl(
        testCipherId,
        testAttachmentId,
        userId,
        { asAdmin: true },
      );

      expect(mockAttachmentsAdminSdk.get_attachment_download_url).toHaveBeenCalledWith(
        testCipherId,
        testAttachmentId,
      );
      expect(mockAttachmentsSdk.get_attachment_download_url).not.toHaveBeenCalled();
      expect(url).toBe(expectedUrl);
    });

    it("passes emergencyAccessId through to the regular SDK method", async () => {
      mockAttachmentsSdk.get_attachment_download_url.mockResolvedValue(expectedUrl);

      const url = await cipherSdkService.getAttachmentDownloadUrl(
        testCipherId,
        testAttachmentId,
        userId,
        { emergencyAccessId: testEmergencyAccessId },
      );

      expect(mockAttachmentsSdk.get_attachment_download_url).toHaveBeenCalledWith(
        testCipherId,
        testAttachmentId,
        testEmergencyAccessId,
      );
      expect(mockAttachmentsAdminSdk.get_attachment_download_url).not.toHaveBeenCalled();
      expect(url).toBe(expectedUrl);
    });

    it("throws when asAdmin and emergencyAccessId are both provided", async () => {
      await expect(
        cipherSdkService.getAttachmentDownloadUrl(testCipherId, testAttachmentId, userId, {
          asAdmin: true,
          emergencyAccessId: testEmergencyAccessId,
        }),
      ).rejects.toThrow("asAdmin and emergencyAccessId are mutually exclusive");
    });

    it("throws and logs when the SDK throws", async () => {
      mockAttachmentsSdk.get_attachment_download_url.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.getAttachmentDownloadUrl(testCipherId, testAttachmentId, userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to get attachment download URL"),
      );
    });
  });

  describe("createAttachment()", () => {
    const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
    const request: CreateAttachmentRequest = {
      key: "2.encryptedKey" as unknown as EncString,
      fileName: "2.encryptedFileName" as unknown as EncString,
      fileSize: 65,
      lastKnownRevisionDate: "2024-05-31T11:20:58.456Z",
      asAdmin: false,
    };
    const created = {
      attachmentId: "newatt9999999999999999999999999",
      uploadUrl: "https://example.com/upload",
      fileUploadType: "Direct" as const,
    };

    it("delegates to SDK attachments.create_attachment and returns its result", async () => {
      mockAttachmentsSdk.create_attachment.mockResolvedValue(created);

      const result = await cipherSdkService.createAttachment(testCipherId, request, userId);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.attachments).toHaveBeenCalled();
      expect(mockAttachmentsSdk.create_attachment).toHaveBeenCalledWith(testCipherId, request);
      expect(result).toBe(created);
    });

    it("throws and logs when the SDK throws", async () => {
      mockAttachmentsSdk.create_attachment.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.createAttachment(testCipherId, request, userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to create attachment"),
      );
    });
  });

  describe("renewAttachmentUploadUrl()", () => {
    const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
    const testAttachmentId = "uf7bkexzag04d3cw04jsbqqkbpbwhxs0";
    const renewedUrl = "https://example.com/upload/renewed";

    it("delegates to SDK attachments.renew_file_upload_url and returns the URL", async () => {
      mockAttachmentsSdk.renew_file_upload_url.mockResolvedValue(renewedUrl);

      const result = await cipherSdkService.renewAttachmentUploadUrl(
        testCipherId,
        testAttachmentId,
        userId,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.attachments).toHaveBeenCalled();
      expect(mockAttachmentsSdk.renew_file_upload_url).toHaveBeenCalledWith(
        testCipherId,
        testAttachmentId,
      );
      expect(result).toBe(renewedUrl);
    });

    it("throws and logs when the SDK throws", async () => {
      mockAttachmentsSdk.renew_file_upload_url.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.renewAttachmentUploadUrl(testCipherId, testAttachmentId, userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to renew attachment upload URL"),
      );
    });
  });

  describe("upgradeAttachment()", () => {
    const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
    const testAttachmentId = "uf7bkexzag04d3cw04jsbqqkbpbwhxs0";

    it("delegates to SDK attachments.upgrade_attachment", async () => {
      mockAttachmentsSdk.upgrade_attachment.mockResolvedValue(undefined);

      await cipherSdkService.upgradeAttachment(testCipherId, testAttachmentId, userId);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.attachments).toHaveBeenCalled();
      expect(mockAttachmentsSdk.upgrade_attachment).toHaveBeenCalledWith(
        testCipherId,
        testAttachmentId,
      );
    });

    it("throws and logs when the SDK throws", async () => {
      mockAttachmentsSdk.upgrade_attachment.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.upgradeAttachment(testCipherId, testAttachmentId, userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to upgrade attachment"),
      );
    });
  });

  describe("getAllDecrypted()", () => {
    it("should list and decrypt ciphers using SDK", async () => {
      const mockSdkCipherView = new CipherView().toSdkCipherView();
      mockSdkCipherView.name = "Test Cipher";
      mockCiphersSdk.get_all.mockResolvedValue({
        successes: [mockSdkCipherView],
        failures: [],
      });

      const result = await cipherSdkService.getAllDecrypted(userId);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.get_all).toHaveBeenCalled();
      expect(result.successes).toHaveLength(1);
      expect(result.successes[0]).toBeInstanceOf(CipherView);
      expect(result.failures).toHaveLength(0);
    });

    it("should return failures with decryptionFailure flag set", async () => {
      // Create a minimal mock that matches what fromSdkCipher expects
      const mockFailedCipher: any = {
        id: cipherId,
        name: "2.encryptedName|iv|data",
        type: CipherType.Login,
        organizationId: null,
        folderId: null,
        favorite: false,
        edit: true,
        viewPassword: true,
        organizationUseTotp: false,
        revisionDate: new Date().toISOString(),
        collectionIds: [],
        deletedDate: null,
        reprompt: 0,
        key: null,
        localData: null,
        attachments: null,
        fields: null,
        passwordHistory: null,
        creationDate: new Date().toISOString(),
        login: null,
        secureNote: null,
        card: null,
        identity: null,
        sshKey: null,
      };
      mockCiphersSdk.get_all.mockResolvedValue({
        successes: [],
        failures: [mockFailedCipher],
      });

      const result = await cipherSdkService.getAllDecrypted(userId);

      expect(result.successes).toHaveLength(0);
      expect(result.failures).toHaveLength(1);
      expect(result.failures[0].decryptionFailure).toBe(true);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.get_all.mockRejectedValue(new Error("SDK error"));

      await expect(cipherSdkService.getAllDecrypted(userId)).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to list and decrypt ciphers"),
      );
    });
  });

  describe("getAllFromApiForOrganization()", () => {
    const mockSdkCipher: any = {
      id: cipherId,
      name: "2.encryptedName|iv|data",
      type: CipherType.Login,
      organizationId: orgId,
      folderId: null,
      favorite: false,
      edit: true,
      viewPassword: true,
      organizationUseTotp: false,
      revisionDate: new Date().toISOString(),
      creationDate: new Date().toISOString(),
      collectionIds: [],
      deletedDate: null,
      reprompt: 0,
      key: null,
      localData: null,
      attachments: null,
      fields: null,
      passwordHistory: null,
      notes: null,
      login: null,
      secureNote: null,
      card: null,
      identity: null,
      sshKey: null,
      permissions: null,
    };

    it("should list organization ciphers using SDK admin API", async () => {
      const mockListView: any = { id: cipherId, name: "Org Cipher" };
      mockAdminSdk.list_org_ciphers.mockResolvedValue({
        ciphers: [mockSdkCipher],
        listViews: [mockListView],
      });

      const result = await cipherSdkService.getAllFromApiForOrganization(orgId, userId, false);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(orgId, false);
      const [ciphers, listViews] = result;
      expect(ciphers).toHaveLength(1);
      expect(ciphers[0]).toBeInstanceOf(Cipher);
      expect(listViews).toHaveLength(1);
    });

    it("should pass includeMemberItems parameter to SDK", async () => {
      mockAdminSdk.list_org_ciphers.mockResolvedValue({
        ciphers: [],
        listViews: [],
      });

      await cipherSdkService.getAllFromApiForOrganization(orgId, userId, true);

      expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(orgId, true);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockAdminSdk.list_org_ciphers.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.getAllFromApiForOrganization(orgId, userId, false),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to list organization ciphers"),
      );
    });
  });

  describe("saveCollectionsWithServerAdmin()", () => {
    const collectionId1 = "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CollectionId;
    const collectionId2 = "7ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b24" as CollectionId;

    it("should update cipher collections using the admin SDK", async () => {
      const collectionIds = [collectionId1, collectionId2];
      const mockSdkCipherView = createMockSdkCipherView(cipherId, collectionIds);
      mockAdminSdk.update_collection.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.saveCollectionsWithServerAdmin(
        cipherId,
        collectionIds,
        userId,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.update_collection).toHaveBeenCalledWith(cipherId, collectionIds);
      expect(result).toBeInstanceOf(CipherView);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockAdminSdk.update_collection.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.saveCollectionsWithServerAdmin(cipherId, [collectionId1], userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to update cipher collections as admin"),
      );
    });
  });

  describe("saveCollectionsWithServer()", () => {
    const collectionId1 = "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CollectionId;
    const collectionId2 = "7ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b24" as CollectionId;

    it("should update cipher collections using the regular SDK client", async () => {
      const collectionIds = [collectionId1, collectionId2];
      const mockSdkCipherView = createMockSdkCipherView(cipherId, collectionIds);
      mockCiphersSdk.update_collection.mockResolvedValue(mockSdkCipherView);

      const result = await cipherSdkService.saveCollectionsWithServer(
        cipherId,
        collectionIds,
        userId,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.update_collection).toHaveBeenCalledWith(cipherId, collectionIds, false);
      expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
      expect(result).toBeInstanceOf(CipherView);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.update_collection.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.saveCollectionsWithServer(cipherId, [collectionId1], userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to update cipher collections"),
      );
    });
  });

  describe("getManyFromApiForOrganization()", () => {
    const mockSdkCipher: any = {
      id: cipherId,
      name: "2.encryptedName|iv|data",
      type: CipherType.Login,
      organizationId: orgId,
      folderId: null,
      favorite: false,
      edit: true,
      viewPassword: true,
      organizationUseTotp: false,
      revisionDate: new Date().toISOString(),
      creationDate: new Date().toISOString(),
      collectionIds: [],
      deletedDate: null,
      reprompt: 0,
      key: null,
      localData: null,
      attachments: null,
      fields: null,
      passwordHistory: null,
      notes: null,
      login: null,
      secureNote: null,
      card: null,
      identity: null,
      sshKey: null,
      permissions: null,
    };

    it("should list assigned organization ciphers using SDK admin API", async () => {
      const mockListView: any = { id: cipherId, name: "Org Cipher" };
      mockAdminSdk.list_assigned_org_ciphers.mockResolvedValue({
        ciphers: [mockSdkCipher],
        listViews: [mockListView],
      });

      const result = await cipherSdkService.getManyFromApiForOrganization(orgId, userId);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.admin).toHaveBeenCalled();
      expect(mockAdminSdk.list_assigned_org_ciphers).toHaveBeenCalledWith(orgId);
      const [ciphers, listViews] = result;
      expect(ciphers).toHaveLength(1);
      expect(ciphers[0]).toBeInstanceOf(Cipher);
      expect(listViews).toHaveLength(1);
    });

    it("should return empty arrays when SDK returns no ciphers", async () => {
      mockAdminSdk.list_assigned_org_ciphers.mockResolvedValue({
        ciphers: [],
        listViews: [],
      });

      const [ciphers, listViews] = await cipherSdkService.getManyFromApiForOrganization(
        orgId,
        userId,
      );

      expect(ciphers).toHaveLength(0);
      expect(listViews).toHaveLength(0);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockAdminSdk.list_assigned_org_ciphers.mockRejectedValue(new Error("SDK error"));

      await expect(cipherSdkService.getManyFromApiForOrganization(orgId, userId)).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to list assigned organization ciphers"),
      );
    });
  });

  describe("bulkUpdateCollectionsWithServer()", () => {
    const collectionId1 = "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CollectionId;
    const collectionId2 = "7ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b24" as CollectionId;
    const cipherId1 = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
    const cipherId2 = "8ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b25" as CipherId;

    it("should add collections via SDK when removeCollections is false", async () => {
      await cipherSdkService.bulkUpdateCollectionsWithServer(
        orgId,
        userId,
        [cipherId1, cipherId2],
        [collectionId1, collectionId2],
        false,
      );

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.bulk_update_collections).toHaveBeenCalledWith(
        orgId,
        [cipherId1, cipherId2],
        [collectionId1, collectionId2],
        false,
      );
    });

    it("should remove collections via SDK when removeCollections is true", async () => {
      await cipherSdkService.bulkUpdateCollectionsWithServer(
        orgId,
        userId,
        [cipherId1],
        [collectionId1],
        true,
      );

      expect(mockCiphersSdk.bulk_update_collections).toHaveBeenCalledWith(
        orgId,
        [cipherId1],
        [collectionId1],
        true,
      );
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.bulk_update_collections.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.bulkUpdateCollectionsWithServer(
          orgId,
          userId,
          [cipherId1],
          [collectionId1],
          false,
        ),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to bulk update cipher collections"),
      );
    });
  });

  describe("moveManyWithServer()", () => {
    const folderId = "9ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b26";
    const cipherIds = [
      "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22",
      "8ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b25",
    ];

    it("should move ciphers to a folder via SDK", async () => {
      await cipherSdkService.moveManyWithServer(cipherIds, folderId, userId);

      expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
      expect(mockVaultSdk.ciphers).toHaveBeenCalled();
      expect(mockCiphersSdk.move_many).toHaveBeenCalledWith(cipherIds, folderId);
    });

    it("should pass undefined when folderId is null (clear folder)", async () => {
      await cipherSdkService.moveManyWithServer(cipherIds, null, userId);

      expect(mockCiphersSdk.move_many).toHaveBeenCalledWith(cipherIds, undefined);
    });

    it("should throw error and log when SDK throws an error", async () => {
      mockCiphersSdk.move_many.mockRejectedValue(new Error("SDK error"));

      await expect(
        cipherSdkService.moveManyWithServer(cipherIds, folderId, userId),
      ).rejects.toThrow();
      expect(logService.error).toHaveBeenCalledWith(
        expect.stringContaining("Failed to move multiple ciphers"),
      );
    });
  });
});
