/*
 * Copyright 2025 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BehaviorSubject } from "rxjs";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createClientMockHelper, mockObserver, waitForCall } from "../testUtils.js";
import { createCollectionConnectable as actualCreateCollectionConnectable } from "./createCollectionConnectable.js";

// Test payload type for our collection

// Mock object instances for testing
const mockEmployee1 = {
  $apiName: "Employee",
  $primaryKey: 1,
  $objectType: "Employee",
  $title: "Employee 1",
  employeeId: 1,
  fullName: "John Doe"
};
const mockEmployee2 = {
  $apiName: "Employee",
  $primaryKey: 2,
  $objectType: "Employee",
  $title: "Employee 2",
  employeeId: 2,
  fullName: "Jane Smith"
};
const mockEmployee3 = {
  $apiName: "Employee",
  $primaryKey: 3,
  $objectType: "Employee",
  $title: "Employee 3",
  employeeId: 3,
  fullName: "Bob Johnson"
};
describe("createCollectionConnectable", () => {
  let mockSubjects;
  let createPayload;
  let mockListCacheKey;
  beforeEach(() => {
    const {
      client
    } = createClientMockHelper();
    mockSubjects = {
      get: vi.fn()
    };

    // Mock cache key for list
    mockListCacheKey = {
      type: "list",
      otherKeys: ["Employee", {}, {}]
    };

    // Create payload function that mimics real usage
    createPayload = vi.fn(params => ({
      data: params.resolvedData,
      status: params.status,
      isOptimistic: params.isOptimistic,
      lastUpdated: params.lastUpdated,
      count: params.resolvedData.length
    }));
  });
  describe("basic functionality", () => {
    it("should create a connectable observable with resolved data", async () => {
      // Mock object cache keys
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const objectKey2 = {
        type: "object",
        otherKeys: ["Employee", 2, undefined],
        __cacheKey: {}
      };

      // Create subject payload with mock data
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1, objectKey2]
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(subjectPayload);

      // Mock store.getSubject to return object observables
      vi.mocked(mockSubjects.get).mockImplementation(cacheKey => {
        if (cacheKey === objectKey1) {
          return new BehaviorSubject({
            cacheKey: objectKey1,
            value: mockEmployee1,
            status: "loaded",
            lastUpdated: Date.now(),
            isOptimistic: false
          });
        }
        if (cacheKey === objectKey2) {
          return new BehaviorSubject({
            cacheKey: objectKey2,
            value: mockEmployee2,
            status: "loaded",
            lastUpdated: Date.now(),
            isOptimistic: false
          });
        }
        return new BehaviorSubject(undefined);
      });
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);

      // Connect and subscribe
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);

      // Wait for the observable to emit
      await waitForCall(observer);

      // Verify the result
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1, mockEmployee2],
        status: "loaded",
        isOptimistic: false,
        count: 2
      }));
      subscription.unsubscribe();
    });
    it("should handle empty data correctly", async () => {
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: []
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(subjectPayload);
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);
      await waitForCall(observer);
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [],
        status: "loaded",
        isOptimistic: false,
        count: 0
      }));
      subscription.unsubscribe();
    });
    it("should handle null/undefined data", async () => {
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: null
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(subjectPayload);
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);
      await waitForCall(observer);
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [],
        status: "loaded",
        isOptimistic: false,
        count: 0
      }));
      subscription.unsubscribe();
    });
  });
  describe("optimistic updates", () => {
    it("should propagate optimistic flag correctly", async () => {
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1]
        },
        status: "loading",
        lastUpdated: Date.now(),
        isOptimistic: true // This is an optimistic update
      };
      const subject = new BehaviorSubject(subjectPayload);
      vi.mocked(mockSubjects.get).mockReturnValue(new BehaviorSubject({
        cacheKey: objectKey1,
        value: mockEmployee1,
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      }));
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);
      await waitForCall(observer);
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1],
        status: "loading",
        isOptimistic: true,
        // Should preserve optimistic flag
        count: 1
      }));
      subscription.unsubscribe();
    });
  });
  describe("status transitions", () => {
    it("should handle different status values", async () => {
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const basePayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1]
        },
        status: "init",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(basePayload);
      vi.mocked(mockSubjects.get).mockReturnValue(new BehaviorSubject({
        cacheKey: objectKey1,
        value: mockEmployee1,
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      }));
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);

      // Wait for initial emission
      await waitForCall(observer);
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1],
        status: "init",
        isOptimistic: false,
        count: 1
      }));
      observer.next.mockClear();

      // Test transition to loading
      subject.next({
        ...basePayload,
        status: "loading",
        lastUpdated: Date.now()
      });
      await waitForCall(observer);
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1],
        status: "loading",
        isOptimistic: false,
        count: 1
      }));
      subscription.unsubscribe();
    });
  });
  describe("multiple objects", () => {
    it("should handle multiple objects in collection", async () => {
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const objectKey2 = {
        type: "object",
        otherKeys: ["Employee", 2, undefined],
        __cacheKey: {}
      };
      const objectKey3 = {
        type: "object",
        otherKeys: ["Employee", 3, undefined],
        __cacheKey: {}
      };
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1, objectKey2, objectKey3]
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(subjectPayload);

      // Mock store.getSubject for all three objects
      vi.mocked(mockSubjects.get).mockImplementation(cacheKey => {
        if (cacheKey === objectKey1) {
          return new BehaviorSubject({
            cacheKey: objectKey1,
            value: mockEmployee1,
            status: "loaded",
            lastUpdated: Date.now(),
            isOptimistic: false
          });
        }
        if (cacheKey === objectKey2) {
          return new BehaviorSubject({
            cacheKey: objectKey2,
            value: mockEmployee2,
            status: "loaded",
            lastUpdated: Date.now(),
            isOptimistic: false
          });
        }
        if (cacheKey === objectKey3) {
          return new BehaviorSubject({
            cacheKey: objectKey3,
            value: mockEmployee3,
            status: "loaded",
            lastUpdated: Date.now(),
            isOptimistic: false
          });
        }
        return new BehaviorSubject(undefined);
      });
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);
      await waitForCall(observer);
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1, mockEmployee2, mockEmployee3],
        status: "loaded",
        isOptimistic: false,
        count: 3
      }));
      subscription.unsubscribe();
    });
    it("should handle objects being added/removed from collection", async () => {
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const objectKey2 = {
        type: "object",
        otherKeys: ["Employee", 2, undefined],
        __cacheKey: {}
      };

      // Start with one object
      const initialPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1]
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(initialPayload);
      vi.mocked(mockSubjects.get).mockImplementation(cacheKey => {
        if (cacheKey === objectKey1) {
          return new BehaviorSubject({
            cacheKey: objectKey1,
            value: mockEmployee1,
            status: "loaded",
            lastUpdated: Date.now(),
            isOptimistic: false
          });
        }
        if (cacheKey === objectKey2) {
          return new BehaviorSubject({
            cacheKey: objectKey2,
            value: mockEmployee2,
            status: "loaded",
            lastUpdated: Date.now(),
            isOptimistic: false
          });
        }
        return new BehaviorSubject(undefined);
      });
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);

      // First check - single object
      await waitForCall(observer);
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1],
        count: 1
      }));
      observer.next.mockClear();

      // Add second object
      subject.next({
        ...initialPayload,
        value: {
          data: [objectKey1, objectKey2]
        },
        lastUpdated: Date.now()
      });

      // Second check - two objects
      await waitForCall(observer);
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1, mockEmployee2],
        count: 2
      }));
      subscription.unsubscribe();
    });
  });
  describe("subscription behavior", () => {
    it("should support multiple subscribers", async () => {
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1]
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(subjectPayload);
      vi.mocked(mockSubjects.get).mockReturnValue(new BehaviorSubject({
        cacheKey: objectKey1,
        value: mockEmployee1,
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      }));
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer1 = mockObserver();
      const observer2 = mockObserver();
      connectable.subscribe(observer1);
      connectable.subscribe(observer2);
      await waitForCall(observer1);
      await waitForCall(observer2);

      // Both observers should receive the data
      expect(observer1.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1],
        count: 1
      }));
      expect(observer2.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1],
        count: 1
      }));
      subscription.unsubscribe();
    });
    it("should use ReplaySubject behavior (last value replayed)", async () => {
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1]
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(subjectPayload);
      vi.mocked(mockSubjects.get).mockReturnValue(new BehaviorSubject({
        cacheKey: objectKey1,
        value: mockEmployee1,
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      }));
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer1 = mockObserver();

      // Subscribe first observer
      connectable.subscribe(observer1);
      await waitForCall(observer1);
      expect(observer1.next).toHaveBeenCalledTimes(1);
      observer1.next.mockClear();

      // Subscribe second observer after first has already received data
      const observer2 = mockObserver();
      connectable.subscribe(observer2);
      await waitForCall(observer2);

      // Second observer should immediately receive the last value (ReplaySubject behavior)
      expect(observer2.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [mockEmployee1],
        count: 1
      }));

      // First observer should not receive additional calls
      expect(observer1.next).not.toHaveBeenCalled();
      subscription.unsubscribe();
    });
  });
  describe("error handling", () => {
    it("should handle store.getSubject returning undefined object entry", async () => {
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1]
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(subjectPayload);

      // Mock store.getSubject to return undefined entry - this creates an object with undefined value
      vi.mocked(mockSubjects.get).mockReturnValue(new BehaviorSubject({
        cacheKey: objectKey1,
        value: undefined,
        // This is the key - undefined value
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      }));
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, createPayload);
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);
      await waitForCall(observer);

      // Should handle undefined object entry gracefully by filtering it out
      expect(observer.next).toHaveBeenCalledWith(expect.objectContaining({
        data: [undefined],
        // The function maps the undefined value from the store
        status: "loaded",
        count: 1
      }));
      subscription.unsubscribe();
    });
    it("should handle createPayload function throwing errors", async () => {
      const objectKey1 = {
        type: "object",
        otherKeys: ["Employee", 1, undefined],
        __cacheKey: {}
      };
      const subjectPayload = {
        cacheKey: mockListCacheKey,
        value: {
          data: [objectKey1]
        },
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      };
      const subject = new BehaviorSubject(subjectPayload);
      vi.mocked(mockSubjects.get).mockReturnValue(new BehaviorSubject({
        cacheKey: objectKey1,
        value: mockEmployee1,
        status: "loaded",
        lastUpdated: Date.now(),
        isOptimistic: false
      }));

      // Mock createPayload to throw an error
      const errorThrowingCreatePayload = vi.fn(() => {
        throw new Error("Test error in createPayload");
      });
      const connectable = actualCreateCollectionConnectable(subject, mockSubjects, errorThrowingCreatePayload);
      const subscription = connectable.connect();
      const observer = mockObserver();
      connectable.subscribe(observer);
      await waitForCall(observer.error);

      // Should call the error handler
      expect(observer.error).toHaveBeenCalledWith(expect.objectContaining({
        message: "Test error in createPayload"
      }));
      subscription.unsubscribe();
    });
  });
});
//# sourceMappingURL=createCollectionConnectable.test.js.map