import { describe, test, expect } from "vitest";
import { InitData, InitDataProvider, InitQuery } from "../shared";

import Context from "./Context";
import splits from "../../test/splits";
import commitConfig from "../../test/commitConfig";
import reducedExpression from "../../test/expression";
import Logger from "./Logger";

const traceId = "test-trace";
const token = "test-token";
const initDataRefreshIntervalMs = 10_000;
const initQuery: InitQuery = { type: "StoredQuery", id: "test-query" };
const variableValues = {};
const logger = new Logger({
  id: "test-logger",
  traceId,
  token,
  remoteLoggingMode: "off",
  remoteFlushIntervalMs: null,
  remoteLoggingEndpointUrl: "",
  localLogger: () => {
    // Noop
  },
  logsHandler: () => {
    // Noop
  },
});
const initData: InitData = {
  commitId: 1,
  hash: "1",
  splits,
  commitConfig,
  reducedExpression,
};

function newContext(
  options: Partial<{
    initData: InitData | null;
    initDataProvider: InitDataProvider | null;
    shouldRefreshInitDataOnCreate: boolean;
    shouldSkipInitDataUpdateOnRefresh: boolean;
  }>
): Context {
  return new Context({
    traceId,
    initData: null,
    lastInitDataRefreshTime: null,
    initDataProvider: null,
    initDataRefreshIntervalMs,
    shouldRefreshInitData: false,
    shouldRefreshInitDataOnCreate: false,
    shouldSkipInitDataUpdateOnRefresh: false,
    query: null,
    initQuery,
    variableValues,
    logger,
    cacheSize: 100,
    override: null,
    ...options,
  });
}

function newInitDataProvider(
  initialCommitId: number
): InitDataProvider & { getCallCount: () => number } {
  let commitId = initialCommitId;

  return {
    getName: () => "testProvider",
    getCallCount: () => commitId - initialCommitId,
    getInitData: () => {
      const data = { ...initData, commitId, hash: commitId.toString() };
      commitId += 1;
      return Promise.resolve(data);
    },
    getHashData: () => {
      const data = { commitId, hash: commitId.toString() };
      commitId += 1;
      return Promise.resolve(data);
    },
  };
}

describe("Context", () => {
  describe("readiness", () => {
    describe("no init data provider", () => {
      test("no initial data", () => {
        const context = newContext({});
        let listenerCallCount = 0;
        context.addUpdateListener((hash, meta) => {
          listenerCallCount += 1;
          expect(meta).toEqual({
            becameReady: listenerCallCount === 1,
            updateTrigger: "hydration",
            hasUpdated: true,
          });
        });
        expect(context.isReady()).toBe(false);
        context.hydrate(traceId, {
          initData,
          lastInitDataRefreshTime: null,
          override: null,
          variableValues: {},
        });
        expect(context.isReady()).toBe(true);
        context.hydrate(traceId, {
          initData,
          lastInitDataRefreshTime: null,
          override: null,
          variableValues: {},
        });
        expect(listenerCallCount).toBe(1);
      });
      test("with initial data", () => {
        const context = newContext({ initData });
        expect(context.isReady()).toBe(true);
      });
    });
    describe("with init data provider", () => {
      test("no initial data and hydration", () => {
        const initDataProvider = newInitDataProvider(initData.commitId);
        const context = newContext({ initDataProvider });

        expect(context.isReady()).toBe(false);
        context.hydrate(traceId, {
          initData,
          lastInitDataRefreshTime: null,
          override: null,
          variableValues: {},
        });
        expect(context.isReady()).toBe(false);
        context.hydrate(traceId, {
          initData,
          lastInitDataRefreshTime: Date.now(),
          override: null,
          variableValues: {},
        });
        expect(context.isReady()).toBe(true);
        expect(initDataProvider.getCallCount()).toBe(0);
      });
      test("with initial data and initialization", async () => {
        const initDataProvider = newInitDataProvider(initData.commitId);
        const context = newContext({ initData, initDataProvider });
        let listenerCallCount = 0;
        context.addUpdateListener((hash, meta) => {
          listenerCallCount += 1;
          expect(meta).toEqual({
            becameReady: listenerCallCount === 1,
            updateTrigger: "initDataProvider",
            hasUpdated: false,
          });
        });

        expect(context.isReady()).toBe(false);
        await context.initIfNeeded(traceId, 1);
        expect(context.isReady()).toBe(true);
        context.hydrate(traceId, {
          initData,
          lastInitDataRefreshTime: Date.now(),
          override: null,
          variableValues: {},
        });
        expect(listenerCallCount).toBe(1);
        expect(initDataProvider.getCallCount()).toBe(1);
      });
      test("with initial data and outdated provider", async () => {
        const initDataProvider = newInitDataProvider(-10);
        const context = newContext({ initData, initDataProvider });
        let listenerCalled = false;
        context.addUpdateListener(() => {
          listenerCalled = true;
        });

        expect(context.isReady()).toBe(false);
        await context.initIfNeeded(traceId, 1);
        expect(context.isReady()).toBe(false);
        expect(listenerCalled).toBe(false);
        expect(initDataProvider.getCallCount()).toBe(1);
      });
    });
  });
  describe("override", () => {
    test("update triggers notification", () => {
      const context = newContext({});
      let listenerCallCount = 0;
      context.addUpdateListener((hash, meta) => {
        listenerCallCount += 1;
        expect(meta).toEqual({
          becameReady: false,
          updateTrigger: "override",
          hasUpdated: true,
        });
      });
      context.setOverride(traceId, { root: {} });
      context.setOverride(traceId, { root: {} });
      expect(listenerCallCount).toBe(1);
    });
  });
  test("shouldSkipInitDataUpdateOnRefresh", async () => {
    const initDataProvider = newInitDataProvider(initData.commitId + 1);
    const context = newContext({
      initData,
      initDataProvider,
      shouldRefreshInitDataOnCreate: true,
      shouldSkipInitDataUpdateOnRefresh: true,
    });
    let listenerCallCount = 0;
    context.addUpdateListener((hash, meta) => {
      expect(meta).toEqual({
        becameReady: false,
        updateTrigger: "initDataProvider",
        hasUpdated: false,
      });
      listenerCallCount += 1;
    });
    await context.initIfNeeded(traceId, 1);
    expect(listenerCallCount).toBe(1);
    expect(initDataProvider.getCallCount()).toBe(1);
  });
});
