import { MessageLoaderUtils } from "modules/ASRouter.sys.mjs";
const { STARTPAGE_VERSION } = MessageLoaderUtils;

const FAKE_OPTIONS = {
  storage: {
    set() {
      return Promise.resolve();
    },
    get() {
      return Promise.resolve();
    },
  },
  dispatchToAS: () => {},
};
const FAKE_RESPONSE_HEADERS = { get() {} };

describe("MessageLoaderUtils", () => {
  let fetchStub;
  let clock;
  let sandbox;

  beforeEach(() => {
    sandbox = sinon.createSandbox();
    clock = sinon.useFakeTimers();
    fetchStub = sinon.stub(global, "fetch");
  });
  afterEach(() => {
    sandbox.restore();
    clock.restore();
    fetchStub.restore();
  });

  describe("#loadMessagesForProvider", () => {
    it("should return messages for a local provider with hardcoded messages", async () => {
      const sourceMessage = { id: "foo" };
      const provider = {
        id: "provider123",
        type: "local",
        messages: [sourceMessage],
      };

      const result = await MessageLoaderUtils.loadMessagesForProvider(
        provider,
        FAKE_OPTIONS
      );

      assert.isArray(result.messages);
      // Does the message have the right properties?
      const [message] = result.messages;
      assert.propertyVal(message, "id", "foo");
      assert.propertyVal(message, "provider", "provider123");
    });
    it("should filter out local messages listed in the `exclude` field", async () => {
      const sourceMessage = { id: "foo" };
      const provider = {
        id: "provider123",
        type: "local",
        messages: [sourceMessage],
        exclude: ["foo"],
      };

      const result = await MessageLoaderUtils.loadMessagesForProvider(
        provider,
        FAKE_OPTIONS
      );

      assert.lengthOf(result.messages, 0);
    });
    it("should return messages for remote provider", async () => {
      const sourceMessage = { id: "foo" };
      fetchStub.resolves({
        ok: true,
        status: 200,
        json: () => Promise.resolve({ messages: [sourceMessage] }),
        headers: FAKE_RESPONSE_HEADERS,
      });
      const provider = {
        id: "provider123",
        type: "remote",
        url: "https://foo.com",
      };

      const result = await MessageLoaderUtils.loadMessagesForProvider(
        provider,
        FAKE_OPTIONS
      );
      assert.isArray(result.messages);
      // Does the message have the right properties?
      const [message] = result.messages;
      assert.propertyVal(message, "id", "foo");
      assert.propertyVal(message, "provider", "provider123");
      assert.propertyVal(message, "provider_url", "https://foo.com");
    });
    describe("remote provider HTTP codes", () => {
      const testMessage = { id: "foo" };
      const provider = {
        id: "provider123",
        type: "remote",
        url: "https://foo.com",
        updateCycleInMs: 300,
      };
      const respJson = { messages: [testMessage] };

      function assertReturnsCorrectMessages(actual) {
        assert.isArray(actual.messages);
        // Does the message have the right properties?
        const [message] = actual.messages;
        assert.propertyVal(message, "id", testMessage.id);
        assert.propertyVal(message, "provider", provider.id);
        assert.propertyVal(message, "provider_url", provider.url);
      }

      it("should return messages for 200 response", async () => {
        fetchStub.resolves({
          ok: true,
          status: 200,
          json: () => Promise.resolve(respJson),
          headers: FAKE_RESPONSE_HEADERS,
        });
        assertReturnsCorrectMessages(
          await MessageLoaderUtils.loadMessagesForProvider(
            provider,
            FAKE_OPTIONS
          )
        );
      });

      it("should return messages for a 302 response with json", async () => {
        fetchStub.resolves({
          ok: true,
          status: 302,
          json: () => Promise.resolve(respJson),
          headers: FAKE_RESPONSE_HEADERS,
        });
        assertReturnsCorrectMessages(
          await MessageLoaderUtils.loadMessagesForProvider(
            provider,
            FAKE_OPTIONS
          )
        );
      });

      it("should return an empty array for a 204 response", async () => {
        fetchStub.resolves({
          ok: true,
          status: 204,
          json: () => "",
          headers: FAKE_RESPONSE_HEADERS,
        });
        const result = await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          FAKE_OPTIONS
        );
        assert.deepEqual(result.messages, []);
      });

      it("should return an empty array for a 500 response", async () => {
        fetchStub.resolves({
          ok: false,
          status: 500,
          json: () => "",
          headers: FAKE_RESPONSE_HEADERS,
        });
        const result = await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          FAKE_OPTIONS
        );
        assert.deepEqual(result.messages, []);
      });

      it("should return cached messages for a 304 response", async () => {
        clock.tick(302);
        const messages = [{ id: "message-1" }, { id: "message-2" }];
        const fakeStorage = {
          set() {
            return Promise.resolve();
          },
          get() {
            return Promise.resolve({
              [provider.id]: {
                version: STARTPAGE_VERSION,
                url: provider.url,
                messages,
                etag: "etag0987654321",
                lastFetched: 1,
              },
            });
          },
        };
        fetchStub.resolves({
          ok: true,
          status: 304,
          json: () => "",
          headers: FAKE_RESPONSE_HEADERS,
        });
        const result = await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          { ...FAKE_OPTIONS, storage: fakeStorage }
        );
        assert.equal(result.messages.length, messages.length);
        messages.forEach(message => {
          assert.ok(result.messages.find(m => m.id === message.id));
        });
      });

      it("should return an empty array if json doesn't parse properly", async () => {
        fetchStub.resolves({
          ok: false,
          status: 200,
          json: () => "",
          headers: FAKE_RESPONSE_HEADERS,
        });
        const result = await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          FAKE_OPTIONS
        );
        assert.deepEqual(result.messages, []);
      });

      it("should report response parsing errors with MessageLoaderUtils.reportError", async () => {
        const err = {};
        sandbox.spy(MessageLoaderUtils, "reportError");
        fetchStub.resolves({
          ok: true,
          status: 200,
          json: sandbox.stub().rejects(err),
          headers: FAKE_RESPONSE_HEADERS,
        });
        await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          FAKE_OPTIONS
        );

        assert.calledOnce(MessageLoaderUtils.reportError);
        // Report that json parsing failed
        assert.calledWith(MessageLoaderUtils.reportError, err);
      });

      it("should report missing `messages` with MessageLoaderUtils.reportError", async () => {
        sandbox.spy(MessageLoaderUtils, "reportError");
        fetchStub.resolves({
          ok: true,
          status: 200,
          json: sandbox.stub().resolves({}),
          headers: FAKE_RESPONSE_HEADERS,
        });
        await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          FAKE_OPTIONS
        );

        assert.calledOnce(MessageLoaderUtils.reportError);
        // Report no messages returned
        assert.calledWith(
          MessageLoaderUtils.reportError,
          "No messages returned from https://foo.com."
        );
      });

      it("should report bad status responses with MessageLoaderUtils.reportError", async () => {
        sandbox.spy(MessageLoaderUtils, "reportError");
        fetchStub.resolves({
          ok: false,
          status: 500,
          json: sandbox.stub().resolves({}),
          headers: FAKE_RESPONSE_HEADERS,
        });
        await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          FAKE_OPTIONS
        );

        assert.calledOnce(MessageLoaderUtils.reportError);
        // Report no messages returned
        assert.calledWith(
          MessageLoaderUtils.reportError,
          "Invalid response status 500 from https://foo.com."
        );
      });

      it("should return an empty array if the request rejects", async () => {
        fetchStub.rejects(new Error("something went wrong"));
        const result = await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          FAKE_OPTIONS
        );
        assert.deepEqual(result.messages, []);
      });
    });
    describe("remote provider caching", () => {
      const provider = {
        id: "provider123",
        type: "remote",
        url: "https://foo.com",
        updateCycleInMs: 300,
      };

      it("should return cached results if they aren't expired", async () => {
        clock.tick(1);
        const messages = [{ id: "message-1" }, { id: "message-2" }];
        const fakeStorage = {
          set() {
            return Promise.resolve();
          },
          get() {
            return Promise.resolve({
              [provider.id]: {
                version: STARTPAGE_VERSION,
                url: provider.url,
                messages,
                etag: "etag0987654321",
                lastFetched: Date.now(),
              },
            });
          },
        };
        const result = await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          { ...FAKE_OPTIONS, storage: fakeStorage }
        );
        assert.equal(result.messages.length, messages.length);
        messages.forEach(message => {
          assert.ok(result.messages.find(m => m.id === message.id));
        });
      });

      it("should return fetch results if the cache messages are expired", async () => {
        clock.tick(302);
        const testMessage = { id: "foo" };
        const respJson = { messages: [testMessage] };
        const fakeStorage = {
          set() {
            return Promise.resolve();
          },
          get() {
            return Promise.resolve({
              [provider.id]: {
                version: STARTPAGE_VERSION,
                url: provider.url,
                messages: [{ id: "message-1" }, { id: "message-2" }],
                etag: "etag0987654321",
                lastFetched: 1,
              },
            });
          },
        };
        fetchStub.resolves({
          ok: true,
          status: 200,
          json: () => Promise.resolve(respJson),
          headers: FAKE_RESPONSE_HEADERS,
        });
        const result = await MessageLoaderUtils.loadMessagesForProvider(
          provider,
          { ...FAKE_OPTIONS, storage: fakeStorage }
        );
        assert.equal(result.messages.length, 1);
        assert.equal(result.messages[0].id, testMessage.id);
      });
    });
    it("should return an empty array for a remote provider with a blank URL without attempting a request", async () => {
      const provider = { id: "provider123", type: "remote", url: "" };

      const result = await MessageLoaderUtils.loadMessagesForProvider(
        provider,
        FAKE_OPTIONS
      );

      assert.notCalled(fetchStub);
      assert.deepEqual(result.messages, []);
    });
    it("should return .lastUpdated with the time at which the messages were fetched", async () => {
      const sourceMessage = { id: "foo" };
      const provider = {
        id: "provider123",
        type: "remote",
        url: "foo.com",
      };

      fetchStub.resolves({
        ok: true,
        status: 200,
        json: () =>
          new Promise(resolve => {
            clock.tick(42);
            resolve({ messages: [sourceMessage] });
          }),
        headers: FAKE_RESPONSE_HEADERS,
      });

      const result = await MessageLoaderUtils.loadMessagesForProvider(
        provider,
        FAKE_OPTIONS
      );

      assert.propertyVal(result, "lastUpdated", 42);
    });
  });

  describe("#shouldProviderUpdate", () => {
    it("should return true if the provider does not had a .lastUpdated property", () => {
      assert.isTrue(MessageLoaderUtils.shouldProviderUpdate({ id: "foo" }));
    });
    it("should return false if the provider does not had a .updateCycleInMs property and has a .lastUpdated", () => {
      clock.tick(1);
      assert.isFalse(
        MessageLoaderUtils.shouldProviderUpdate({ id: "foo", lastUpdated: 0 })
      );
    });
    it("should return true if the time since .lastUpdated is greater than .updateCycleInMs", () => {
      clock.tick(301);
      assert.isTrue(
        MessageLoaderUtils.shouldProviderUpdate({
          id: "foo",
          lastUpdated: 0,
          updateCycleInMs: 300,
        })
      );
    });
    it("should return false if the time since .lastUpdated is less than .updateCycleInMs", () => {
      clock.tick(299);
      assert.isFalse(
        MessageLoaderUtils.shouldProviderUpdate({
          id: "foo",
          lastUpdated: 0,
          updateCycleInMs: 300,
        })
      );
    });
  });

  describe("#cleanupCache", () => {
    it("should remove data for providers no longer active", async () => {
      const fakeStorage = {
        get: sinon.stub().returns(
          Promise.resolve({
            "id-1": {},
            "id-2": {},
            "id-3": {},
          })
        ),
        set: sinon.stub().returns(Promise.resolve()),
      };
      const fakeProviders = [
        { id: "id-1", type: "remote" },
        { id: "id-3", type: "remote" },
      ];

      await MessageLoaderUtils.cleanupCache(fakeProviders, fakeStorage);

      assert.calledOnce(fakeStorage.set);
      assert.calledWith(
        fakeStorage.set,
        MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY,
        { "id-1": {}, "id-3": {} }
      );
    });
  });
});
