import { waitFor } from "@testing-library/dom";
import { createHttpMockingServer } from "@hyper-fetch/testing";

import { Client } from "client";
import { Plugin } from "plugin";

const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer();

describe("Plugin [ Base ]", () => {
  let client = new Client({ url: "shared-base-url" });
  let request = client.createRequest()({ endpoint: "shared-nase-endpoint" });

  beforeAll(() => {
    startServer();
  });

  beforeEach(() => {
    resetMocks();
    vi.resetAllMocks();
    client = new Client({ url: "shared-base-url" });
    request = client.createRequest()({ endpoint: "shared-nase-endpoint" });
  });

  afterAll(() => {
    stopServer();
  });

  describe("When initializing plugin", () => {
    it("should properly set initial values", () => {
      const pluginData = { test: true };
      const plugin = new Plugin({
        name: "test-plugin",
        data: pluginData,
      });

      expect(plugin.name).toBe("test-plugin");
      expect(plugin.data).toEqual(pluginData);
      expect(plugin.client).toBeUndefined();
    });

    it("should initialize with client", () => {
      const plugin = new Plugin({ name: "test-plugin" });
      const initializedPlugin = plugin.initialize(client);

      expect(initializedPlugin.client).toBe(client);
      expect(initializedPlugin).toBe(plugin);
    });
  });

  describe("When using plugin trigger", () => {
    it("should trigger registered method", () => {
      const callback = vi.fn();
      const plugin = new Plugin({ name: "test-plugin" });

      plugin.onDispatcherCleared(callback);
      plugin.trigger("onDispatcherCleared", {
        dispatcher: client.fetchDispatcher,
      });

      expect(callback).toHaveBeenCalledWith({ dispatcher: client.fetchDispatcher });
    });

    it("should not throw when triggering unregistered method", () => {
      const plugin = new Plugin({ name: "test-plugin" });

      expect(() => {
        plugin.trigger("onDispatcherCleared", {} as any);
      }).not.toThrow();
    });
  });

  describe("When using dispatcher effects", () => {
    it("should register dispatcher lifecycle methods", () => {
      const plugin = new Plugin({ name: "test-plugin" });
      const callback = vi.fn();

      const methods = [
        "onDispatcherCleared",
        "onDispatcherQueueDrained",
        "onDispatcherQueueRunning",
        "onDispatcherItemAdded",
        "onDispatcherItemDeleted",
        "onDispatcherQueueCreated",
        "onDispatcherQueueCleared",
      ];

      methods.forEach((method) => {
        const result = (plugin as any)[method](callback);
        expect(result).toBe(plugin);
        plugin.trigger(method as any, {});
        expect(callback).toHaveBeenCalledTimes(1);
        callback.mockClear();
      });
    });
  });

  describe("When using cache effects", () => {
    it("should register cache lifecycle methods", () => {
      const plugin = new Plugin({ name: "test-plugin" });
      const callback = vi.fn();

      const methods = ["onCacheItemChange", "onCacheItemDelete"];

      methods.forEach((method) => {
        const result = (plugin as any)[method](callback as any);
        expect(result).toBe(plugin);
        plugin.trigger(method as any, {});
        expect(callback).toHaveBeenCalledTimes(1);
        callback.mockClear();
      });
    });
  });

  describe("When using adapter effects", () => {
    it("should register adapter fetch method", () => {
      const plugin = new Plugin({ name: "test-plugin" });
      const callback = vi.fn();

      const result = plugin.onAdapterFetch(callback);
      expect(result).toBe(plugin);
      plugin.trigger("onAdapterFetch", {
        request: request as any,
        requestId: "123",
        adapter: client.adapter,
      });
      expect(callback).toHaveBeenCalledTimes(1);
    });
  });

  describe("When using fetch effects", () => {
    it("should trigger success effects", async () => {
      mockRequest(request);
      const spy1 = vi.fn();
      const spy2 = vi.fn();
      const spy3 = vi.fn();
      const spy4 = vi.fn();
      const spy5 = vi.fn();

      const effect = new Plugin({
        name: "123",
      })
        .onRequestError(spy1)
        .onRequestSuccess(spy2)
        .onRequestFinished(spy3)
        .onRequestStart(spy4)
        .onRequestTrigger(spy5);

      client.addPlugin(effect);

      request.send({});

      await waitFor(() => {
        expect(spy1).toHaveBeenCalledTimes(0);
        expect(spy2).toHaveBeenCalledTimes(1);
        expect(spy3).toHaveBeenCalledTimes(1);
        expect(spy4).toHaveBeenCalledTimes(1);
        expect(spy5).toHaveBeenCalledTimes(1);
      });
    });
    it("should trigger error effects", async () => {
      mockRequest(request, { status: 400 });
      const spy1 = vi.fn();
      const spy2 = vi.fn();
      const spy3 = vi.fn();
      const spy4 = vi.fn();
      const spy5 = vi.fn();

      const effect = new Plugin({
        name: "123",
      })
        .onRequestError(spy1)
        .onRequestSuccess(spy2)
        .onRequestFinished(spy3)
        .onRequestStart(spy4)
        .onRequestTrigger(spy5);

      client.addPlugin(effect);

      request.send({});

      await waitFor(() => {
        expect(spy1).toHaveBeenCalledTimes(1);
        expect(spy2).toHaveBeenCalledTimes(0);
        expect(spy3).toHaveBeenCalledTimes(1);
        expect(spy4).toHaveBeenCalledTimes(1);
        expect(spy5).toHaveBeenCalledTimes(1);
      });
    });
    it("should not throw when effects are empty", async () => {
      const effect = new Plugin({
        name: "123",
      });

      expect(effect.onRequestTrigger).not.toThrow();
      expect(effect.onRequestStart).not.toThrow();
      expect(effect.onRequestSuccess).not.toThrow();
      expect(effect.onRequestError).not.toThrow();
      expect(effect.onRequestFinished).not.toThrow();
    });
  });

  describe("When using mount/unmount lifecycle", () => {
    it("should register mount and unmount callbacks", () => {
      const plugin = new Plugin({ name: "test-plugin" });
      const mountCallback = vi.fn();
      const unmountCallback = vi.fn();

      const mountResult = plugin.onMount(mountCallback);
      const unmountResult = plugin.onUnmount(unmountCallback);

      expect(mountResult).toBe(plugin);
      expect(unmountResult).toBe(plugin);

      plugin.trigger("onMount", { client });
      plugin.trigger("onUnmount", { client });

      expect(mountCallback).toHaveBeenCalledWith({ client });
      expect(unmountCallback).toHaveBeenCalledWith({ client });
    });
  });

  describe("When using request lifecycle", () => {
    it("should register request create callback", () => {
      const plugin = new Plugin({ name: "test-plugin" });
      const callback = vi.fn();

      const result = plugin.onRequestCreate(callback);
      expect(result).toBe(plugin);

      plugin.trigger("onRequestCreate", {
        request,
      });

      expect(callback).toHaveBeenCalledWith({
        request,
      });
    });

    it("should register request trigger callback", () => {
      const plugin = new Plugin({ name: "test-plugin" });
      const callback = vi.fn();

      const result = plugin.onRequestTrigger(callback);
      expect(result).toBe(plugin);

      plugin.trigger("onRequestTrigger", {
        request,
      });

      expect(callback).toHaveBeenCalledWith({
        request,
      });
    });
  });
});
