import { beforeEach, describe, expect, vi, test, afterEach } from "vitest";

describe.each([
  {
    library: "@google-cloud/firestore",
    mockFunction: "mockGoogleCloudFirestore",
  },
  {
    library: "@react-native-firebase/firestore",
    mockFunction: "mockReactNativeFirestore",
  },
])("mocking $library with %mockFunction", async ({ library, mockFunction }) => {
  const FirestoreMock = await import("..");

  const flushPromises = async (): Promise<void> => {
    await new Promise(setImmediate);
  };
  const { Timestamp } = await import("../mocks/timestamp");
  const {
    mockGet,
    mockSelect,
    mockAdd,
    mockSet,
    mockUpdate,
    mockWhere,
    mockCollectionGroup,
    mockBatch,
    mockBatchCommit,
    mockBatchDelete,
    mockBatchUpdate,
    mockBatchSet,
    mockSettings,
    mockOnSnapShot,
    mockListCollections,
    mockTimestampNow,
    CollectionReference,
  } = await import("../mocks/firestore");

  describe("we can start a firestore application", () => {
    FirestoreMock[mockFunction]({
      database: {
        users: [
          { id: "abc123", first: "Bob", last: "builder", born: 1998 },
          {
            id: "123abc",
            first: "Blues",
            last: "builder",
            born: 1996,
            _collections: {
              cities: [
                {
                  id: "LA",
                  name: "Los Angeles",
                  state: "CA",
                  country: "USA",
                  visited: true,
                },
              ],
            },
          },
        ],
        cities: [
          { id: "LA", name: "Los Angeles", state: "CA", country: "USA" },
          {
            id: "DC",
            name: "Disctric of Columbia",
            state: "DC",
            country: "USA",
          },
        ],
      },
    });

    let Firestore;

    beforeEach(async () => {
      const package = await import(library);
      Firestore = package.Firestore;
    });

    afterEach(() => mockTimestampNow.mockClear());

    test("We can start an application", async () => {
      const firestore = new Firestore();
      firestore.settings({ ignoreUndefinedProperties: true });
      expect(mockSettings).toHaveBeenCalledWith({
        ignoreUndefinedProperties: true,
      });
    });

    describe("Examples from documentation", () => {
      test("add a user", () => {
        const firestore = new Firestore();

        return firestore
          .collection("users")
          .add({
            first: "Ada",
            last: "Lovelace",
            born: 1815,
          })
          .then(function (docRef) {
            expect(mockAdd).toHaveBeenCalled();
            expect(docRef).toHaveProperty("id");
          });
      });

      test("get all users", () => {
        const firestore = new Firestore();

        return firestore
          .collection("users")
          .get()
          .then((querySnapshot) => {
            expect(querySnapshot.forEach).toBeTruthy();
            expect(querySnapshot.docs.length).toBe(2);
            expect(querySnapshot.size).toBe(querySnapshot.docs.length);

            querySnapshot.forEach((doc) => {
              expect(doc.exists).toBe(true);
              expect(doc.data()).toBeTruthy();
            });
          });
      });

      test("select specific fields only", () => {
        const firestore = new Firestore();

        return firestore
          .collection("users")
          .select("first", "last")
          .get()
          .then((querySnapshot) => {
            expect(mockSelect).toHaveBeenCalledWith("first", "last");

            const data = querySnapshot.docs[0].data();
            expect(Object.keys(data).length).toBe(2);
            expect(data.first).toBe("Bob");
            expect(data.last).toBe("builder");
          });
      });

      test("select refs only", () => {
        const firestore = new Firestore();

        return firestore
          .collection("users")
          .select()
          .get()
          .then((querySnapshot) => {
            expect(mockSelect).toHaveBeenCalledWith();

            const data = querySnapshot.docs[0].data();
            expect(Object.keys(data).length).toBe(0);
          });
      });

      test("collectionGroup at root", () => {
        const firestore = new Firestore();

        return firestore
          .collectionGroup("users")
          .where("last", "==", "builder")
          .get()
          .then((querySnapshot) => {
            expect(mockCollectionGroup).toHaveBeenCalledWith("users");
            expect(mockGet).toHaveBeenCalled();
            expect(mockWhere).toHaveBeenCalledWith("last", "==", "builder");

            expect(querySnapshot.forEach).toBeTruthy();
            expect(querySnapshot.docs.length).toBe(2);
            expect(querySnapshot.size).toBe(querySnapshot.docs.length);

            querySnapshot.forEach((doc) => {
              expect(doc.exists).toBe(true);
              expect(doc.data()).toBeTruthy();
            });
          });
      });

      test("collectionGroup with subcollections", () => {
        vi.clearAllMocks();
        const firestore = new Firestore();

        return firestore
          .collectionGroup("cities")
          .where("country", "==", "USA")
          .get()
          .then((querySnapshot) => {
            expect(mockCollectionGroup).toHaveBeenCalledWith("cities");
            expect(mockGet).toHaveBeenCalledTimes(1);
            expect(mockWhere).toHaveBeenCalledWith("country", "==", "USA");

            expect(querySnapshot.forEach).toBeTruthy();
            expect(querySnapshot.docs.length).toBe(3);
            expect(querySnapshot.size).toBe(querySnapshot.docs.length);

            querySnapshot.forEach((doc) => {
              expect(doc.exists).toBe(true);
              expect(doc.data()).toBeTruthy();
            });
          });
      });

      test("set a city", () => {
        const firestore = new Firestore();

        return firestore
          .collection("cities")
          .doc("LA")
          .set({
            name: "Los Angeles",
            state: "CA",
            country: "USA",
          })
          .then(function () {
            expect(mockSet).toHaveBeenCalledWith({
              name: "Los Angeles",
              state: "CA",
              country: "USA",
            });
          });
      });

      test("updating a city", () => {
        const firestore = new Firestore();
        const now = Timestamp._fromMillis(new Date().getTime());
        const washingtonRef = firestore.collection("cities").doc("DC");

        mockTimestampNow.mockReturnValue(now);

        return washingtonRef
          .update({
            capital: true,
          })
          .then(function (value) {
            expect(value.updateTime).toStrictEqual(now);
            expect(mockUpdate).toHaveBeenCalledWith({ capital: true });
          });
      });

      test("batch writes", () => {
        const firestore = new Firestore();

        // Get a new write batch
        const batch = firestore.batch();

        // Set the value of 'NYC'
        const nycRef = firestore.collection("cities").doc("NYC");
        batch.set(nycRef, { name: "New York City" });

        // Update the population of 'SF'
        const sfRef = firestore.collection("cities").doc("SF");
        batch.update(sfRef, { population: 1000000 });

        // Delete the city 'LA'
        const laRef = firestore.collection("cities").doc("LA");
        batch.delete(laRef);

        // Commit the batch
        return batch.commit().then(function () {
          expect(mockBatch).toHaveBeenCalled();
          expect(mockBatchDelete).toHaveBeenCalledWith(laRef);
          expect(mockBatchUpdate).toHaveBeenCalledWith(sfRef, {
            population: 1000000,
          });
          expect(mockBatchSet).toHaveBeenCalledWith(nycRef, {
            name: "New York City",
          });
          expect(mockBatchCommit).toHaveBeenCalled();
        });
      });

      test("listCollections returns a promise", async () => {
        const firestore = new Firestore();

        const listCollectionsPromise = firestore
          .collection("cities")
          .doc("LA")
          .listCollections();

        expect(listCollectionsPromise).toEqual(expect.any(Promise));
      });

      test("listCollections resolves with child collections", async () => {
        const firestore = new Firestore();

        const result = await firestore
          .collection("users")
          .doc("123abc")
          .listCollections();

        expect(result).toEqual(expect.any(Array));
        expect(result).toHaveLength(1);
        expect(result[0]).toEqual(expect.any(CollectionReference));
        expect(result[0].id).toBe("cities");
      });

      test("listCollections resolves with empty array if there are no collections in document", async () => {
        const firestore = new Firestore();

        const result = await firestore
          .collection("users")
          .doc("abc123")
          .listCollections();

        expect(result).toEqual(expect.any(Array));
        expect(result).toHaveLength(0);
      });

      test("listCollections calls mockListCollections", async () => {
        const firestore = new Firestore();

        await firestore.collection("users").doc("abc123").listCollections();

        expect(mockListCollections).toHaveBeenCalled();
      });

      test("onSnapshot single doc", async () => {
        const firestore = new Firestore();
        const now = Timestamp._fromMillis(new Date().getTime());

        mockTimestampNow.mockReturnValue(now);

        firestore
          .collection("cities")
          .doc("LA")
          .onSnapshot((doc) => {
            expect(doc).toHaveProperty("createTime");
            expect(doc).toHaveProperty("data");
            expect(doc.data).toBeInstanceOf(Function);
            expect(doc).toHaveProperty("metadata");
            expect(doc).toHaveProperty("readTime");
            expect(doc).toHaveProperty("updateTime");
            expect(doc.readTime).toStrictEqual(now);
          });

        await flushPromises();

        expect(mockOnSnapShot).toHaveBeenCalled();
      });

      test("onSnapshot can work with options", async () => {
        const firestore = new Firestore();
        const now = Timestamp._fromMillis(new Date().getTime());

        mockTimestampNow.mockReturnValue(now);

        firestore
          .collection("cities")
          .doc("LA")
          .onSnapshot(
            {
              // Listen for document metadata changes
              includeMetadataChanges: true,
            },
            (doc) => {
              expect(doc).toHaveProperty("createTime");
              expect(doc).toHaveProperty("data");
              expect(doc.data).toBeInstanceOf(Function);
              expect(doc).toHaveProperty("metadata");
              expect(doc).toHaveProperty("readTime");
              expect(doc).toHaveProperty("updateTime");
              expect(doc.readTime).toStrictEqual(now);
            }
          );

        await flushPromises();

        expect(mockOnSnapShot).toHaveBeenCalled();
      });

      test("onSnapshot with query", async () => {
        const firestore = new Firestore();

        const unsubscribe = firestore
          .collection("cities")
          .where("state", "==", "CA")
          .onSnapshot((querySnapshot) => {
            expect(querySnapshot).toHaveProperty(
              "forEach",
              expect.any(Function)
            );
            expect(querySnapshot).toHaveProperty("docChanges");
            expect(querySnapshot).toHaveProperty("docs", expect.any(Array));

            expect(querySnapshot.forEach).toBeInstanceOf(Function);
            expect(querySnapshot.docChanges).toBeInstanceOf(Function);
            expect(querySnapshot.docs).toBeInstanceOf(Array);

            expect(querySnapshot.docChanges()).toBeInstanceOf(Array);
          });

        await flushPromises();

        expect(unsubscribe).toBeInstanceOf(Function);
        expect(mockWhere).toHaveBeenCalled();
        expect(mockOnSnapShot).toHaveBeenCalled();
      });
    });
  });
});
