import { MemberDetails } from "../../models";

import {
  isApplicationHealthReportDetail,
  isMemberDetails,
  isMemberRegistryEntryData,
  isOrganizationReportApplication,
  isOrganizationReportSummary,
  validateApplicationHealthReportDetailArray,
  validateOrganizationReportApplicationArray,
  validateOrganizationReportSummary,
  validateAccessReportSettingsDataArray,
  validateAccessReportSummaryView,
  validateAccessReportPayload,
} from "./risk-insights-type-guards";

describe("Risk Insights Type Guards", () => {
  describe("validateApplicationHealthReportDetailArray", () => {
    it("should return all elements with no errors when input is fully valid", () => {
      const validData = [
        {
          applicationName: "Test App",
          passwordCount: 10,
          atRiskPasswordCount: 2,
          atRiskCipherIds: ["cipher-1", "cipher-2"],
          memberCount: 5,
          atRiskMemberCount: 1,
          memberDetails: [
            {
              userGuid: "user-1",
              userName: "John Doe",
              email: "john@example.com",
              cipherId: "cipher-1",
            },
          ],
          atRiskMemberDetails: [
            {
              userGuid: "user-2",
              userName: "Jane Doe",
              email: "jane@example.com",
              cipherId: "cipher-2",
            },
          ],
          cipherIds: ["cipher-1", "cipher-2"],
        },
      ];

      const result = validateApplicationHealthReportDetailArray(validData);
      expect(result.data).toEqual(validData);
      expect(result.errors).toEqual([]);
    });

    it("should throw error for non-array input", () => {
      expect(() => validateApplicationHealthReportDetailArray("not an array")).toThrow(
        "Invalid report data: expected array of ApplicationHealthReportDetail, received non-array",
      );
    });

    it("should drop invalid elements and report them in errors", () => {
      const invalidData = [
        {
          applicationName: "Test App",
          // missing required fields
        },
      ];

      const result = validateApplicationHealthReportDetailArray(invalidData);
      expect(result.data).toEqual([]);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toMatch(/element\[0\]/);
    });

    it("should keep valid elements and drop invalid ones in a mixed array", () => {
      const validElement = {
        applicationName: "App 2",
        passwordCount: 10,
        atRiskPasswordCount: 2,
        atRiskCipherIds: ["cipher-1"],
        memberCount: 5,
        atRiskMemberCount: 1,
        memberDetails: [] as MemberDetails[],
        atRiskMemberDetails: [] as MemberDetails[],
        cipherIds: ["cipher-1"],
      };
      const invalidData = [
        { applicationName: "App 1" }, // invalid
        validElement,
        { applicationName: "App 3" }, // invalid
      ];

      const result = validateApplicationHealthReportDetailArray(invalidData);
      expect(result.data).toEqual([validElement]);
      expect(result.errors).toHaveLength(2);
      expect(result.errors[0]).toMatch(/element\[0\]/);
      expect(result.errors[1]).toMatch(/element\[2\]/);
    });

    it("should drop element with invalid memberDetails", () => {
      const invalidData = [
        {
          applicationName: "Test App",
          passwordCount: 10,
          atRiskPasswordCount: 2,
          atRiskCipherIds: ["cipher-1"],
          memberCount: 5,
          atRiskMemberCount: 1,
          memberDetails: [{ userGuid: "user-1" }] as any, // missing required fields
          atRiskMemberDetails: [] as MemberDetails[],
          cipherIds: ["cipher-1"],
        },
      ];

      const result = validateApplicationHealthReportDetailArray(invalidData);
      expect(result.data).toEqual([]);
      expect(result.errors).toHaveLength(1);
    });

    it("should drop element with empty string in atRiskCipherIds", () => {
      const invalidData = [
        {
          applicationName: "Test App",
          passwordCount: 10,
          atRiskPasswordCount: 2,
          atRiskCipherIds: ["cipher-1", "", "cipher-3"], // empty string
          memberCount: 5,
          atRiskMemberCount: 1,
          memberDetails: [] as MemberDetails[],
          atRiskMemberDetails: [] as MemberDetails[],
          cipherIds: ["cipher-1"],
        },
      ];

      const result = validateApplicationHealthReportDetailArray(invalidData);
      expect(result.data).toEqual([]);
      expect(result.errors).toHaveLength(1);
    });

    it("should drop element with empty string in cipherIds", () => {
      const invalidData = [
        {
          applicationName: "Test App",
          passwordCount: 10,
          atRiskPasswordCount: 2,
          atRiskCipherIds: ["cipher-1"],
          memberCount: 5,
          atRiskMemberCount: 1,
          memberDetails: [] as MemberDetails[],
          atRiskMemberDetails: [] as MemberDetails[],
          cipherIds: ["", "cipher-2"], // empty string
        },
      ];

      const result = validateApplicationHealthReportDetailArray(invalidData);
      expect(result.data).toEqual([]);
      expect(result.errors).toHaveLength(1);
    });

    it("should drop element with empty applicationName (the PM-37079 case)", () => {
      const invalidData = [
        {
          applicationName: "", // empty — fails isBoundedString
          passwordCount: 1,
          atRiskPasswordCount: 0,
          atRiskCipherIds: [] as string[],
          memberCount: 1,
          atRiskMemberCount: 0,
          memberDetails: [] as MemberDetails[],
          atRiskMemberDetails: [] as MemberDetails[],
          cipherIds: ["cipher-1"],
        },
      ];

      const result = validateApplicationHealthReportDetailArray(invalidData);
      expect(result.data).toEqual([]);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toMatch(/applicationName/);
    });
  });

  describe("validateOrganizationReportSummary", () => {
    it("should return summary with no errors when input is fully valid", () => {
      const validData = {
        totalMemberCount: 10,
        totalApplicationCount: 5,
        totalAtRiskMemberCount: 2,
        totalAtRiskApplicationCount: 1,
        totalCriticalApplicationCount: 3,
        totalCriticalMemberCount: 4,
        totalCriticalAtRiskMemberCount: 1,
        totalCriticalAtRiskApplicationCount: 1,
      };

      const result = validateOrganizationReportSummary(validData);
      expect(result.data).toMatchObject(validData);
      expect(result.errors).toEqual([]);
    });

    it("should default missing password count fields to 0 for old blobs", () => {
      const oldBlob = {
        totalMemberCount: 10,
        totalApplicationCount: 5,
        totalAtRiskMemberCount: 2,
        totalAtRiskApplicationCount: 1,
        totalCriticalApplicationCount: 3,
        totalCriticalMemberCount: 4,
        totalCriticalAtRiskMemberCount: 1,
        totalCriticalAtRiskApplicationCount: 1,
        // password count fields absent — as in blobs written before this change
      };

      const result = validateOrganizationReportSummary(oldBlob);
      expect(result.data.totalPasswordCount).toBe(0);
      expect(result.data.totalAtRiskPasswordCount).toBe(0);
      expect(result.data.totalCriticalPasswordCount).toBe(0);
      expect(result.data.totalCriticalAtRiskPasswordCount).toBe(0);
      // Missing optional fields are normalized silently — no errors recorded.
      expect(result.errors).toEqual([]);
    });

    it("should preserve password count fields when present", () => {
      const newBlob = {
        totalMemberCount: 10,
        totalApplicationCount: 5,
        totalAtRiskMemberCount: 2,
        totalAtRiskApplicationCount: 1,
        totalCriticalApplicationCount: 3,
        totalCriticalMemberCount: 4,
        totalCriticalAtRiskMemberCount: 1,
        totalCriticalAtRiskApplicationCount: 1,
        totalPasswordCount: 20,
        totalAtRiskPasswordCount: 5,
        totalCriticalPasswordCount: 10,
        totalCriticalAtRiskPasswordCount: 3,
      };

      const result = validateOrganizationReportSummary(newBlob);
      expect(result.data.totalPasswordCount).toBe(20);
      expect(result.data.totalAtRiskPasswordCount).toBe(5);
      expect(result.data.totalCriticalPasswordCount).toBe(10);
      expect(result.data.totalCriticalAtRiskPasswordCount).toBe(3);
    });

    it("should default invalid field types to 0 and report them in errors", () => {
      const invalidData = {
        totalMemberCount: "10", // should be number
        totalApplicationCount: 5,
        totalAtRiskMemberCount: 2,
        totalAtRiskApplicationCount: 1,
        totalCriticalApplicationCount: 3,
        totalCriticalMemberCount: 4,
        totalCriticalAtRiskMemberCount: 1,
        totalCriticalAtRiskApplicationCount: 1,
      };

      const result = validateOrganizationReportSummary(invalidData);
      expect(result.data.totalMemberCount).toBe(0);
      expect(result.data.totalApplicationCount).toBe(5);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toMatch(/totalMemberCount/);
    });

    it("should throw error for non-object input", () => {
      expect(() => validateOrganizationReportSummary(null)).toThrow(/Invalid report summary/);
      expect(() => validateOrganizationReportSummary("not an object")).toThrow(
        /Invalid report summary/,
      );
      expect(() => validateOrganizationReportSummary([])).toThrow(/Invalid report summary/);
    });
  });

  describe("validateOrganizationReportApplicationArray", () => {
    it("should return all elements with no errors when input is fully valid", () => {
      const validData = [
        {
          applicationName: "Test App",
          isCritical: true,
          reviewedDate: null,
        },
        {
          applicationName: "Another App",
          isCritical: false,
          reviewedDate: new Date("2024-01-01"),
        },
      ];

      const result = validateOrganizationReportApplicationArray(validData);
      expect(result.data[0].applicationName).toBe("Test App");
      expect(result.data[1].reviewedDate).toBeInstanceOf(Date);
      expect(result.errors).toEqual([]);
    });

    it("should convert string dates to Date objects", () => {
      const validData = [
        {
          applicationName: "Test App",
          isCritical: true,
          reviewedDate: "2024-01-01T00:00:00.000Z",
        },
      ];

      const result = validateOrganizationReportApplicationArray(validData);
      expect(result.data[0].reviewedDate).toBeInstanceOf(Date);
      expect(result.data[0].reviewedDate?.toISOString()).toBe("2024-01-01T00:00:00.000Z");
    });

    it("should drop element with invalid date string", () => {
      const invalidData = [
        {
          applicationName: "Test App",
          isCritical: true,
          reviewedDate: "invalid-date",
        },
      ];

      const result = validateOrganizationReportApplicationArray(invalidData);
      expect(result.data).toEqual([]);
      expect(result.errors).toHaveLength(1);
    });

    it("should throw error for non-array input", () => {
      expect(() => validateOrganizationReportApplicationArray("not an array")).toThrow(
        "Invalid application data: expected array of OrganizationReportApplication, received non-array",
      );
    });

    it("should drop element missing required field", () => {
      const invalidData = [
        {
          applicationName: "Test App",
          reviewedDate: null as any,
          // missing isCritical field
        } as any,
      ];

      const result = validateOrganizationReportApplicationArray(invalidData);
      expect(result.data).toEqual([]);
      expect(result.errors).toHaveLength(1);
    });

    it("should drop element with invalid field types", () => {
      const invalidData = [
        {
          applicationName: 123 as any, // should be string
          isCritical: true,
          reviewedDate: null as any,
        } as any,
      ];

      const result = validateOrganizationReportApplicationArray(invalidData);
      expect(result.data).toEqual([]);
      expect(result.errors).toHaveLength(1);
    });

    it("should keep valid elements and drop invalid ones in a mixed array", () => {
      const mixedData = [
        { applicationName: "Good App", isCritical: true, reviewedDate: null as Date | null },
        { applicationName: "" as any, isCritical: true, reviewedDate: null as Date | null }, // empty name — bad
        { applicationName: "Another Good", isCritical: false, reviewedDate: null as Date | null },
      ];

      const result = validateOrganizationReportApplicationArray(mixedData);
      expect(result.data).toHaveLength(2);
      expect(result.data[0].applicationName).toBe("Good App");
      expect(result.data[1].applicationName).toBe("Another Good");
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toMatch(/element\[1\]/);
    });

    it("should accept null reviewedDate", () => {
      const validData = [
        {
          applicationName: "Test App",
          isCritical: true,
          reviewedDate: null as any,
        },
      ];

      const result = validateOrganizationReportApplicationArray(validData);
      expect(result.data[0].reviewedDate).toBeNull();
      expect(result.errors).toEqual([]);
    });
  });

  // Tests for exported type guard functions
  describe("isMemberDetails", () => {
    it("should return true for valid MemberDetails", () => {
      const validData = {
        userGuid: "user-1",
        userName: "John Doe",
        email: "john@example.com",
        cipherId: "cipher-1",
      };
      expect(isMemberDetails(validData)).toBe(true);
    });

    it("should return false for empty userGuid", () => {
      const invalidData = {
        userGuid: "",
        userName: "John Doe",
        email: "john@example.com",
        cipherId: "cipher-1",
      };
      expect(isMemberDetails(invalidData)).toBe(false);
    });

    it("should return false for empty userName", () => {
      const invalidData = {
        userGuid: "user-1",
        userName: "",
        email: "john@example.com",
        cipherId: "cipher-1",
      };
      expect(isMemberDetails(invalidData)).toBe(false);
    });

    it("should return true for null userName", () => {
      const validData = {
        userGuid: "user-1",
        userName: null as string | null,
        email: "john@example.com",
        cipherId: "cipher-1",
      };
      expect(isMemberDetails(validData)).toBe(true);
    });

    it("should return false for empty email", () => {
      const invalidData = {
        userGuid: "user-1",
        userName: "John Doe",
        email: "",
        cipherId: "cipher-1",
      };
      expect(isMemberDetails(invalidData)).toBe(false);
    });

    it("should return false for empty cipherId", () => {
      const invalidData = {
        userGuid: "user-1",
        userName: "John Doe",
        email: "john@example.com",
        cipherId: "",
      };
      expect(isMemberDetails(invalidData)).toBe(false);
    });

    it("should return false for prototype pollution attempts", () => {
      const invalidData = {
        userGuid: "user-1",
        userName: "John Doe",
        email: "john@example.com",
        cipherId: "cipher-1",
        __proto__: { malicious: "payload" },
      };
      expect(isMemberDetails(invalidData)).toBe(false);
    });
  });

  describe("isApplicationHealthReportDetail", () => {
    it("should return true for valid ApplicationHealthReportDetail", () => {
      const validData = {
        applicationName: "Test App",
        passwordCount: 10,
        atRiskPasswordCount: 2,
        atRiskCipherIds: ["cipher-1"],
        memberCount: 5,
        atRiskMemberCount: 1,
        memberDetails: [] as MemberDetails[],
        atRiskMemberDetails: [] as MemberDetails[],
        cipherIds: ["cipher-1"],
      };
      expect(isApplicationHealthReportDetail(validData)).toBe(true);
    });

    it("should return false for empty applicationName", () => {
      const invalidData = {
        applicationName: "",
        passwordCount: 10,
        atRiskPasswordCount: 2,
        atRiskCipherIds: ["cipher-1"],
        memberCount: 5,
        atRiskMemberCount: 1,
        memberDetails: [] as MemberDetails[],
        atRiskMemberDetails: [] as MemberDetails[],
        cipherIds: ["cipher-1"],
      };
      expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
    });

    it("should return false for NaN passwordCount", () => {
      const invalidData = {
        applicationName: "Test App",
        passwordCount: NaN,
        atRiskPasswordCount: 2,
        atRiskCipherIds: ["cipher-1"],
        memberCount: 5,
        atRiskMemberCount: 1,
        memberDetails: [] as MemberDetails[],
        atRiskMemberDetails: [] as MemberDetails[],
        cipherIds: ["cipher-1"],
      };
      expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
    });

    it("should return false for Infinity passwordCount", () => {
      const invalidData = {
        applicationName: "Test App",
        passwordCount: Infinity,
        atRiskPasswordCount: 2,
        atRiskCipherIds: ["cipher-1"],
        memberCount: 5,
        atRiskMemberCount: 1,
        memberDetails: [] as MemberDetails[],
        atRiskMemberDetails: [] as MemberDetails[],
        cipherIds: ["cipher-1"],
      };
      expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
    });

    it("should return false for negative passwordCount", () => {
      const invalidData = {
        applicationName: "Test App",
        passwordCount: -5,
        atRiskPasswordCount: 2,
        atRiskCipherIds: ["cipher-1"],
        memberCount: 5,
        atRiskMemberCount: 1,
        memberDetails: [] as MemberDetails[],
        atRiskMemberDetails: [] as MemberDetails[],
        cipherIds: ["cipher-1"],
      };
      expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
    });

    it("should return false for negative memberCount", () => {
      const invalidData = {
        applicationName: "Test App",
        passwordCount: 10,
        atRiskPasswordCount: 2,
        atRiskCipherIds: ["cipher-1"],
        memberCount: -1,
        atRiskMemberCount: 1,
        memberDetails: [] as MemberDetails[],
        atRiskMemberDetails: [] as MemberDetails[],
        cipherIds: ["cipher-1"],
      };
      expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
    });
  });

  describe("isOrganizationReportSummary", () => {
    it("should return true for valid OrganizationReportSummary", () => {
      const validData = {
        totalMemberCount: 10,
        totalApplicationCount: 5,
        totalAtRiskMemberCount: 2,
        totalAtRiskApplicationCount: 1,
        totalCriticalApplicationCount: 3,
        totalCriticalMemberCount: 4,
        totalCriticalAtRiskMemberCount: 1,
        totalCriticalAtRiskApplicationCount: 1,
      };
      expect(isOrganizationReportSummary(validData)).toBe(true);
    });

    it("should return false for NaN totalMemberCount", () => {
      const invalidData = {
        totalMemberCount: NaN,
        totalApplicationCount: 5,
        totalAtRiskMemberCount: 2,
        totalAtRiskApplicationCount: 1,
        totalCriticalApplicationCount: 3,
        totalCriticalMemberCount: 4,
        totalCriticalAtRiskMemberCount: 1,
        totalCriticalAtRiskApplicationCount: 1,
      };
      expect(isOrganizationReportSummary(invalidData)).toBe(false);
    });

    it("should return false for Infinity totalApplicationCount", () => {
      const invalidData = {
        totalMemberCount: 10,
        totalApplicationCount: Infinity,
        totalAtRiskMemberCount: 2,
        totalAtRiskApplicationCount: 1,
        totalCriticalApplicationCount: 3,
        totalCriticalMemberCount: 4,
        totalCriticalAtRiskMemberCount: 1,
        totalCriticalAtRiskApplicationCount: 1,
      };
      expect(isOrganizationReportSummary(invalidData)).toBe(false);
    });

    it("should return false for negative totalAtRiskMemberCount", () => {
      const invalidData = {
        totalMemberCount: 10,
        totalApplicationCount: 5,
        totalAtRiskMemberCount: -1,
        totalAtRiskApplicationCount: 1,
        totalCriticalApplicationCount: 3,
        totalCriticalMemberCount: 4,
        totalCriticalAtRiskMemberCount: 1,
        totalCriticalAtRiskApplicationCount: 1,
      };
      expect(isOrganizationReportSummary(invalidData)).toBe(false);
    });
  });

  describe("isOrganizationReportApplication", () => {
    it("should return true for valid OrganizationReportApplication", () => {
      const validData = {
        applicationName: "Test App",
        isCritical: true,
        reviewedDate: null as Date | null,
      };
      expect(isOrganizationReportApplication(validData)).toBe(true);
    });

    it("should return false for empty applicationName", () => {
      const invalidData = {
        applicationName: "",
        isCritical: true,
        reviewedDate: null as Date | null,
      };
      expect(isOrganizationReportApplication(invalidData)).toBe(false);
    });

    it("should return true for Date reviewedDate", () => {
      const validData = {
        applicationName: "Test App",
        isCritical: true,
        reviewedDate: new Date(),
      };
      expect(isOrganizationReportApplication(validData)).toBe(true);
    });

    it("should return true for string reviewedDate", () => {
      const validData = {
        applicationName: "Test App",
        isCritical: false,
        reviewedDate: "2024-01-01",
      };
      expect(isOrganizationReportApplication(validData)).toBe(true);
    });

    it("should return false for prototype pollution attempts via __proto__", () => {
      const invalidData = {
        applicationName: "Test App",
        isCritical: true,
        reviewedDate: null as Date | null,
        __proto__: { polluted: true },
      };
      expect(isOrganizationReportApplication(invalidData)).toBe(false);
    });
  });

  describe("isMemberRegistryEntryData", () => {
    it("should return true for valid MemberRegistryEntryData", () => {
      const validData = { id: "u1", userName: "Alice", email: "alice@example.com" };
      expect(isMemberRegistryEntryData(validData)).toBe(true);
    });

    it("should return false for empty id", () => {
      const invalidData = { id: "", userName: "Alice", email: "alice@example.com" };
      expect(isMemberRegistryEntryData(invalidData)).toBe(false);
    });

    it("should return false for empty userName (empty string is not a valid non-empty string)", () => {
      const invalidData = { id: "u1", userName: "", email: "alice@example.com" };
      expect(isMemberRegistryEntryData(invalidData)).toBe(false);
    });

    it("should return true when userName is absent (userName is optional)", () => {
      const dataWithoutUserName = { id: "u1", email: "alice@example.com" };
      expect(isMemberRegistryEntryData(dataWithoutUserName)).toBe(true);
    });

    it("should return true when userName is undefined (userName is optional)", () => {
      const dataWithUndefinedUserName: unknown = {
        id: "u1",
        userName: undefined,
        email: "alice@example.com",
      };
      expect(isMemberRegistryEntryData(dataWithUndefinedUserName)).toBe(true);
    });

    it("should return false for empty email", () => {
      const invalidData = { id: "u1", userName: "Alice", email: "" };
      expect(isMemberRegistryEntryData(invalidData)).toBe(false);
    });

    it("should return false for missing field", () => {
      const invalidData = { id: "u1", userName: "Alice" };
      expect(isMemberRegistryEntryData(invalidData)).toBe(false);
    });

    it("should return false for non-object", () => {
      expect(isMemberRegistryEntryData("not an object")).toBe(false);
      expect(isMemberRegistryEntryData(null)).toBe(false);
    });

    it("should return false for prototype pollution attempts", () => {
      const invalidData = {
        id: "u1",
        userName: "Alice",
        email: "alice@example.com",
        __proto__: { malicious: true },
      };
      expect(isMemberRegistryEntryData(invalidData)).toBe(false);
    });
  });

  describe("validateAccessReportPayload", () => {
    const validV2Data = {
      version: 2,
      reports: [
        {
          applicationName: "github.com",
          passwordCount: 2,
          atRiskPasswordCount: 1,
          memberRefs: { u1: true, u2: false },
          cipherRefs: { c1: true, c2: false },
          memberCount: 2,
          atRiskMemberCount: 1,
        },
      ],
      memberRegistry: {
        u1: { id: "u1", userName: "Alice", email: "alice@example.com" },
        u2: { id: "u2", userName: "Bob", email: "bob@example.com" },
      },
    };

    it("should validate valid V2 report data", () => {
      expect(() => validateAccessReportPayload(validV2Data)).not.toThrow();
      const result = validateAccessReportPayload(validV2Data);
      expect(result.data.reports).toHaveLength(1);
      expect(Object.keys(result.data.memberRegistry)).toHaveLength(2);
      expect(result.errors).toHaveLength(0);
    });

    it("should validate V2 data with empty reports and registry", () => {
      const emptyV2Data: unknown = { version: 2, reports: [], memberRegistry: {} };
      expect(() => validateAccessReportPayload(emptyV2Data)).not.toThrow();
    });

    it("should throw for non-object input", () => {
      expect(() => validateAccessReportPayload("not an object")).toThrow(
        /expected object, received non-object/,
      );
      expect(() => validateAccessReportPayload(null)).toThrow(
        /expected object, received non-object/,
      );
      expect(() => validateAccessReportPayload([1, 2, 3])).toThrow(
        /expected object, received non-object/,
      );
    });

    it("should throw for non-array reports (structural failure)", () => {
      const invalidReports = { ...validV2Data, reports: "not an array" };
      expect(() => validateAccessReportPayload(invalidReports)).toThrow(
        /reports expected array, received non-array/,
      );
    });

    it("should throw for missing memberRegistry (structural failure)", () => {
      const noRegistry: unknown = { version: 2, reports: [] };
      expect(() => validateAccessReportPayload(noRegistry)).toThrow(
        /memberRegistry expected object, received non-object/,
      );
    });

    it("should drop a bad reports[] element and accumulate one error", () => {
      const badReportElement = {
        ...validV2Data,
        reports: [
          { applicationName: "" }, // empty name + missing fields — invalid
          validV2Data.reports[0], // valid
        ],
      };
      expect(() => validateAccessReportPayload(badReportElement)).not.toThrow();
      const result = validateAccessReportPayload(badReportElement);
      expect(result.data.reports).toHaveLength(1);
      expect(result.data.reports[0].applicationName).toBe("github.com");
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toMatch(/reports\[0\]/);
    });

    it("should drop a bad memberRegistry entry and accumulate one error", () => {
      const invalidEntry = {
        ...validV2Data,
        memberRegistry: {
          u1: { id: "u1", userName: "Alice", email: "alice@example.com" }, // valid
          u2: { id: "u2", userName: "Bob" }, // missing email — invalid
        },
      };
      expect(() => validateAccessReportPayload(invalidEntry)).not.toThrow();
      const result = validateAccessReportPayload(invalidEntry);
      expect(Object.keys(result.data.memberRegistry)).toEqual(["u1"]);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toMatch(/memberRegistry\["u2"\]/);
    });

    it("should normalize empty userName to undefined for backwards compatibility", () => {
      const dataWithEmptyUserName: unknown = {
        version: 2,
        reports: [],
        memberRegistry: {
          u1: { id: "u1", userName: "", email: "alice@example.com" },
        },
      };
      const result = validateAccessReportPayload(dataWithEmptyUserName);
      expect(result.data.memberRegistry["u1"].userName).toBeUndefined();
      expect(result.errors).toHaveLength(0);
    });
  });

  describe("validateAccessReportSummaryView", () => {
    const validSummary = {
      totalMemberCount: 10,
      totalApplicationCount: 5,
      totalAtRiskMemberCount: 2,
      totalAtRiskApplicationCount: 1,
      totalCriticalApplicationCount: 3,
      totalCriticalMemberCount: 4,
      totalCriticalAtRiskMemberCount: 1,
      totalCriticalAtRiskApplicationCount: 1,
    };

    it("should validate valid summary data", () => {
      expect(() => validateAccessReportSummaryView(validSummary)).not.toThrow();
      const result = validateAccessReportSummaryView(validSummary);
      expect(result.totalMemberCount).toBe(10);
      expect(result.totalApplicationCount).toBe(5);
    });

    it("should throw for invalid field types", () => {
      const invalid = { ...validSummary, totalMemberCount: "10" };
      expect(() => validateAccessReportSummaryView(invalid)).toThrow(/Invalid report summary/);
    });

    it("should throw for non-object input", () => {
      expect(() => validateAccessReportSummaryView(null)).toThrow(/Invalid report summary/);
      expect(() => validateAccessReportSummaryView("string")).toThrow(/Invalid report summary/);
    });

    it("should accept old blobs without password count fields", () => {
      // Old encrypted blobs predate password counts — fields are absent, not zero
      expect(() => validateAccessReportSummaryView(validSummary)).not.toThrow();
    });

    it("should accept new blobs with password count fields present", () => {
      const withPasswords = {
        ...validSummary,
        totalPasswordCount: 20,
        totalAtRiskPasswordCount: 5,
        totalCriticalPasswordCount: 10,
        totalCriticalAtRiskPasswordCount: 3,
      };
      expect(() => validateAccessReportSummaryView(withPasswords)).not.toThrow();
    });
  });

  describe("validateAccessReportSettingsDataArray", () => {
    it("should validate valid V2 application data array", () => {
      const validData = [
        { applicationName: "app.com", isCritical: true, reviewedDate: "2024-01-15T10:30:00.000Z" },
        { applicationName: "other.com", isCritical: false },
      ];

      expect(() => validateAccessReportSettingsDataArray(validData)).not.toThrow();
      const result = validateAccessReportSettingsDataArray(validData);
      expect(result.data).toHaveLength(2);
      expect(result.data[0].reviewedDate).toBe("2024-01-15T10:30:00.000Z");
      expect(result.data[1].reviewedDate).toBeUndefined();
      expect(result.errors).toHaveLength(0);
    });

    it("should accept missing reviewedDate (undefined)", () => {
      const validData = [{ applicationName: "app.com", isCritical: false }];
      const result = validateAccessReportSettingsDataArray(validData);
      expect(result.data[0].reviewedDate).toBeUndefined();
    });

    it("should throw for non-array input (structural failure)", () => {
      expect(() => validateAccessReportSettingsDataArray("not an array")).toThrow(
        "Invalid application data: expected array of AccessReportSettingsData, received non-array",
      );
    });

    it("should drop an element with empty applicationName and accumulate one error", () => {
      const invalidData = [
        { applicationName: "", isCritical: true }, // empty name — rejected by isBoundedString
        { applicationName: "app.com", isCritical: false }, // valid
      ];
      expect(() => validateAccessReportSettingsDataArray(invalidData)).not.toThrow();
      const result = validateAccessReportSettingsDataArray(invalidData);
      expect(result.data).toHaveLength(1);
      expect(result.data[0].applicationName).toBe("app.com");
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toMatch(/element\[0\]/);
    });

    it("should drop an element missing isCritical and accumulate one error", () => {
      const invalidData = [{ applicationName: "app.com" }]; // missing isCritical
      const result = validateAccessReportSettingsDataArray(invalidData);
      expect(result.data).toHaveLength(0);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toMatch(/element\[0\]/);
    });

    it("should drop an element with null reviewedDate", () => {
      const invalidData: unknown = [
        { applicationName: "app.com", isCritical: true, reviewedDate: null },
      ];
      const result = validateAccessReportSettingsDataArray(invalidData);
      expect(result.data).toHaveLength(0);
      expect(result.errors).toHaveLength(1);
    });

    it("should drop an element with an invalid date string in reviewedDate", () => {
      const invalidData = [
        { applicationName: "app.com", isCritical: true, reviewedDate: "not-a-date" },
      ];
      const result = validateAccessReportSettingsDataArray(invalidData);
      expect(result.data).toHaveLength(0);
      expect(result.errors).toHaveLength(1);
    });
  });

  describe("field-level diagnostics", () => {
    describe("validateApplicationHealthReportDetailArray", () => {
      it("should include element index and field name in errors", () => {
        const invalidData = [{ applicationName: "Test App" }]; // missing many required fields

        const result = validateApplicationHealthReportDetailArray(invalidData);
        expect(result.errors).toHaveLength(1);
        expect(result.errors[0]).toMatch(/element\[0\]/);
        expect(result.errors[0]).toMatch(/field '/);
      });

      it("should identify memberDetails field failure when memberDetails has wrong shape", () => {
        const invalidData = [
          {
            applicationName: "Test App",
            passwordCount: 10,
            atRiskPasswordCount: 2,
            atRiskCipherIds: ["cipher-1"],
            memberCount: 5,
            atRiskMemberCount: 1,
            memberDetails: [{ userGuid: "user-1" }] as any, // missing required fields
            atRiskMemberDetails: [] as MemberDetails[],
            cipherIds: ["cipher-1"],
          },
        ];

        const result = validateApplicationHealthReportDetailArray(invalidData);
        expect(result.errors).toHaveLength(1);
        expect(result.errors[0]).toContain("element[0]");
        expect(result.errors[0]).toContain("memberDetails");
      });

      it("should list each failing element by index, skipping valid ones", () => {
        const invalidData = [
          { applicationName: "App 1" }, // invalid
          {
            applicationName: "App 2",
            passwordCount: 10,
            atRiskPasswordCount: 2,
            atRiskCipherIds: ["cipher-1"],
            memberCount: 5,
            atRiskMemberCount: 1,
            memberDetails: [] as MemberDetails[],
            atRiskMemberDetails: [] as MemberDetails[],
            cipherIds: ["cipher-1"],
          }, // valid
          { applicationName: "App 3" }, // invalid
        ];

        const result = validateApplicationHealthReportDetailArray(invalidData);
        expect(result.data).toHaveLength(1);
        expect(result.data[0].applicationName).toBe("App 2");
        expect(result.errors).toHaveLength(2);
        expect(result.errors[0]).toContain("element[0]");
        expect(result.errors[1]).toContain("element[2]");
      });
    });

    describe("validateAccessReportPayload — memberRegistry diagnostics", () => {
      it("should include the registry key and failing field name when a registry entry is invalid", () => {
        const invalidPayload = {
          version: 2,
          reports: [] as unknown[],
          memberRegistry: {
            u1: { id: "u1", userName: "Alice" }, // missing email
          },
        };

        const result = validateAccessReportPayload(invalidPayload);
        expect(result.data.memberRegistry).toEqual({});
        expect(result.errors).toHaveLength(1);
        expect(result.errors[0]).toContain('"u1"');
        expect(result.errors[0]).toContain("email");
      });
    });
  });
});
