import { build } from ".";
import JsHistogram from "./JsHistogram";
import { NO_TAG } from "./Histogram";
import Int32Histogram from "./Int32Histogram";
import { initWebAssembly, WasmHistogram, initWebAssemblySync } from "./wasm";
import Int8Histogram from "./Int8Histogram";
import Histogram from "./Histogram";

class HistogramForTests extends JsHistogram {
  //constructor() {}

  clearCounts() {}

  incrementCountAtIndex(index: number): void {}

  incrementTotalCount(): void {}

  addToTotalCount(value: number) {}

  setTotalCount(totalCount: number) {}

  resize(newHighestTrackableValue: number): void {
    this.establishSize(newHighestTrackableValue);
  }

  addToCountAtIndex(index: number, value: number): void {}

  setCountAtIndex(index: number, value: number): void {}

  getTotalCount() {
    return 0;
  }

  getCountAtIndex(index: number): number {
    return 0;
  }

  protected _getEstimatedFootprintInBytes() {
    return 42;
  }

  copyCorrectedForCoordinatedOmission(
    expectedIntervalBetweenValueSamples: number
  ) {
    return this;
  }
}

describe("Histogram initialization", () => {
  let histogram: JsHistogram;
  beforeEach(() => {
    histogram = new HistogramForTests(1, Number.MAX_SAFE_INTEGER, 3);
  });

  it("should set sub bucket size", () => {
    expect(histogram.subBucketCount).toBe(2048);
  });

  it("should set resize to false when max value specified", () => {
    expect(histogram.autoResize).toBe(false);
  });

  it("should compute counts array length", () => {
    expect(histogram.countsArrayLength).toBe(45056);
  });

  it("should compute bucket count", () => {
    expect(histogram.bucketCount).toBe(43);
  });

  it("should set min non zero value", () => {
    expect(histogram.minNonZeroValue).toBe(Number.MAX_SAFE_INTEGER);
  });

  it("should set max value", () => {
    expect(histogram.maxValue).toBe(0);
  });
});

describe("Histogram recording values", () => {
  it("should compute count index when value in first bucket", () => {
    // given
    const histogram = new HistogramForTests(1, Number.MAX_SAFE_INTEGER, 3);
    // when
    const index = histogram.countsArrayIndex(2000); // 2000 < 2048
    expect(index).toBe(2000);
  });

  it("should compute count index when value outside first bucket", () => {
    // given
    const histogram = new HistogramForTests(1, Number.MAX_SAFE_INTEGER, 3);
    // when
    const index = histogram.countsArrayIndex(2050); // 2050 > 2048
    // then
    expect(index).toBe(2049);
  });

  it("should compute count index taking into account lowest discernible value", () => {
    // given
    const histogram = new HistogramForTests(2000, Number.MAX_SAFE_INTEGER, 2);
    // when
    const index = histogram.countsArrayIndex(16000);
    // then
    expect(index).toBe(15);
  });

  it("should compute count index of a big value taking into account lowest discernible value", () => {
    // given
    const histogram = new HistogramForTests(2000, Number.MAX_SAFE_INTEGER, 2);
    // when
    const bigValue = Number.MAX_SAFE_INTEGER - 1;
    const index = histogram.countsArrayIndex(bigValue);
    // then
    expect(index).toBe(4735);
  });

  it("should update min non zero value", () => {
    // given
    const histogram = new HistogramForTests(1, Number.MAX_SAFE_INTEGER, 3);
    // when
    histogram.recordValue(123);
    // then
    expect(histogram.minNonZeroValue).toBe(123);
  });

  it("should update max value", () => {
    // given
    const histogram = new HistogramForTests(1, Number.MAX_SAFE_INTEGER, 3);
    // when
    histogram.recordValue(123);
    // then
    expect(histogram.maxValue).toBe(123);
  });

  it("should throw an error when value bigger than highest trackable value", () => {
    // given
    const histogram = new HistogramForTests(1, 4096, 3);
    // when then
    expect(() => histogram.recordValue(9000)).toThrowError();
  });

  it("should not throw an error when autoresize enable and value bigger than highest trackable value", () => {
    // given
    const histogram = new HistogramForTests(1, 4096, 3);
    histogram.autoResize = true;
    // when then
    expect(() => histogram.recordValue(9000)).not.toThrowError();
  });

  it("should increase counts array size when recording value bigger than highest trackable value", () => {
    // given
    const histogram = new HistogramForTests(1, 4096, 3);
    histogram.autoResize = true;
    // when
    histogram.recordValue(9000);
    // then
    expect(histogram.highestTrackableValue).toBeGreaterThan(9000);
  });
});

describe("Histogram computing statistics", () => {
  const histogram = new Int32Histogram(1, Number.MAX_SAFE_INTEGER, 3);

  it("should compute mean value", () => {
    // given
    histogram.reset();
    // when
    histogram.recordValue(25);
    histogram.recordValue(50);
    histogram.recordValue(75);
    // then
    expect(histogram.mean).toBe(50);
  });

  it("should compute standard deviation", () => {
    // given
    histogram.reset();
    // when
    histogram.recordValue(25);
    histogram.recordValue(50);
    histogram.recordValue(75);
    // then
    expect(histogram.stdDeviation).toBeGreaterThan(20.4124);
    expect(histogram.stdDeviation).toBeLessThan(20.4125);
  });

  it("should compute percentile distribution", () => {
    // given
    histogram.reset();
    // when
    histogram.recordValue(25);
    histogram.recordValue(50);
    histogram.recordValue(75);
    // then
    const expectedResult = `       Value     Percentile TotalCount 1/(1-Percentile)

      25.000 0.000000000000          1           1.00
      25.000 0.100000000000          1           1.11
      25.000 0.200000000000          1           1.25
      25.000 0.300000000000          1           1.43
      50.000 0.400000000000          2           1.67
      50.000 0.500000000000          2           2.00
      50.000 0.550000000000          2           2.22
      50.000 0.600000000000          2           2.50
      50.000 0.650000000000          2           2.86
      75.000 0.700000000000          3           3.33
      75.000 1.000000000000          3
#[Mean    =       50.000, StdDeviation   =       20.412]
#[Max     =       75.000, Total count    =            3]
#[Buckets =           43, SubBuckets     =         2048]
`;
    expect(histogram.outputPercentileDistribution()).toBe(expectedResult);
  });

  it("should compute percentile distribution in csv format", () => {
    // given
    histogram.reset();
    // when
    histogram.recordValue(25);
    histogram.recordValue(50);
    histogram.recordValue(75);
    // then
    const expectedResult = `"Value","Percentile","TotalCount","1/(1-Percentile)"
25.000,0.000000000000,1,1.00
25.000,0.100000000000,1,1.11
25.000,0.200000000000,1,1.25
25.000,0.300000000000,1,1.43
50.000,0.400000000000,2,1.67
50.000,0.500000000000,2,2.00
50.000,0.550000000000,2,2.22
50.000,0.600000000000,2,2.50
50.000,0.650000000000,2,2.86
75.000,0.700000000000,3,3.33
75.000,1.000000000000,3,Infinity
`;
    expect(
      histogram.outputPercentileDistribution(undefined, undefined, true)
    ).toBe(expectedResult);
  });

  it("should compute percentile distribution in JSON format with rounding according to number of significant digits", () => {
    // given
    histogram.reset();
    // when
    histogram.recordValue(25042);
    histogram.recordValue(50042);
    histogram.recordValue(75042);
    // then
    const { summary } = histogram;
    expect(summary.p50).toEqual(50000);
  });
});

describe("Histogram correcting coordinated omissions", () => {
  const histogram = new Int32Histogram(1, Number.MAX_SAFE_INTEGER, 3);

  it("should generate additional values when recording", () => {
    // given
    histogram.reset();
    // when
    histogram.recordValueWithExpectedInterval(207, 100);
    // then
    expect(histogram.totalCount).toBe(2);
    expect(histogram.minNonZeroValue).toBe(107);
    expect(histogram.maxValue).toBe(207);
  });

  it("should generate additional values when correcting after recording", () => {
    // given
    histogram.reset();
    histogram.recordValue(207);
    histogram.recordValue(207);
    // when
    const correctedHistogram = histogram.copyCorrectedForCoordinatedOmission(
      100
    );
    // then
    expect(correctedHistogram.totalCount).toBe(4);
    expect(correctedHistogram.minNonZeroValue).toBe(107);
    expect(correctedHistogram.maxValue).toBe(207);
  });

  it("should not generate additional values when correcting after recording", () => {
    // given
    histogram.reset();
    histogram.recordValue(207);
    histogram.recordValue(207);
    // when
    const correctedHistogram = histogram.copyCorrectedForCoordinatedOmission(
      1000
    );
    // then
    expect(correctedHistogram.totalCount).toBe(2);
    expect(correctedHistogram.minNonZeroValue).toBe(207);
    expect(correctedHistogram.maxValue).toBe(207);
  });
});

describe("WASM Histogram not initialized", () => {
  it("should throw a clear error message", () => {
    expect(() => build({ useWebAssembly: true })).toThrow(
      "WebAssembly is not ready yet"
    );
    expect(() => WasmHistogram.build()).toThrow("WebAssembly is not ready yet");
    expect(() => WasmHistogram.decode(null as any)).toThrow(
      "WebAssembly is not ready yet"
    );
  });
});

describe("WASM Histogram not happy path", () => {
  beforeEach(initWebAssemblySync);
  it("should throw a clear error message when used after destroy", () => {
    const destroyedHistogram = build({ useWebAssembly: true });
    destroyedHistogram.destroy();
    expect(() => destroyedHistogram.recordValue(42)).toThrow(
      "Cannot use a destroyed histogram"
    );
  });
  it("should not crash when displayed after destroy", () => {
    const destroyedHistogram = build({ useWebAssembly: true });
    destroyedHistogram.destroy();
    expect(destroyedHistogram + "").toEqual("Destroyed WASM histogram");
  });
  it("should throw a clear error message when added to a JS regular Histogram", () => {
    const wasmHistogram = build({ useWebAssembly: true });
    const jsHistogram = build({ useWebAssembly: false });
    expect(() => jsHistogram.add(wasmHistogram)).toThrow(
      "Cannot add a WASM histogram to a regular JS histogram"
    );
  });
  it("should throw a clear error message when trying to add a JS regular Histogram", () => {
    const wasmHistogram = build({ useWebAssembly: true });
    const jsHistogram = build({ useWebAssembly: false });
    expect(() => wasmHistogram.add(jsHistogram)).toThrow(
      "Cannot add a regular JS histogram to a WASM histogram"
    );
  });

  it("should throw a clear error message when substracted to a JS regular Histogram", () => {
    const wasmHistogram = build({ useWebAssembly: true });
    const jsHistogram = build({ useWebAssembly: false });
    expect(() => jsHistogram.subtract(wasmHistogram)).toThrow(
      "Cannot subtract a WASM histogram to a regular JS histogram"
    );
  });
  it("should throw a clear error message when trying to add a JS regular Histogram", () => {
    const wasmHistogram = build({ useWebAssembly: true });
    const jsHistogram = build({ useWebAssembly: false });
    expect(() => wasmHistogram.subtract(jsHistogram)).toThrow(
      "Cannot subtract a regular JS histogram to a WASM histogram"
    );
  });
});

describe("WASM estimated memory footprint", () => {
  let wasmHistogram: Histogram;
  beforeAll(initWebAssembly);
  afterEach(() => wasmHistogram.destroy());

  it("should be a little bit more than js footprint for packed histograms", () => {
    wasmHistogram = build({ useWebAssembly: true, bitBucketSize: "packed" });
    expect(wasmHistogram.estimatedFootprintInBytes).toBeGreaterThan(
      build({ bitBucketSize: "packed" }).estimatedFootprintInBytes
    );
  });
});

describe("WASM Histogram correcting coordinated omissions", () => {
  let histogram: Histogram;

  beforeAll(initWebAssembly);
  beforeEach(() => {
    histogram = build({ useWebAssembly: true });
  });
  afterEach(() => histogram.destroy());

  it("should generate additional values when recording", () => {
    // given
    histogram.reset();
    // when
    histogram.recordValueWithExpectedInterval(207, 100);
    // then
    expect(histogram.totalCount).toBe(2);
    expect(histogram.minNonZeroValue).toBe(107);
    expect(histogram.maxValue).toBe(207);
  });

  it("should generate additional values when correcting after recording", () => {
    // given
    histogram.reset();
    histogram.recordValue(207);
    histogram.recordValue(207);
    // when
    const correctedHistogram = histogram.copyCorrectedForCoordinatedOmission(
      100
    );
    // then
    expect(correctedHistogram.totalCount).toBe(4);
    expect(correctedHistogram.minNonZeroValue).toBe(107);
    expect(correctedHistogram.maxValue).toBe(207);
  });

  it("should not generate additional values when correcting after recording", () => {
    // given
    histogram.reset();
    histogram.recordValue(207);
    histogram.recordValue(207);
    // when
    const correctedHistogram = histogram.copyCorrectedForCoordinatedOmission(
      1000
    );
    // then
    expect(correctedHistogram.totalCount).toBe(2);
    expect(correctedHistogram.minNonZeroValue).toBe(207);
    expect(correctedHistogram.maxValue).toBe(207);
  });
});

describe("Histogram add & substract", () => {
  beforeAll(initWebAssembly);

  it("should add histograms of same size", () => {
    // given
    const histogram = new Int32Histogram(1, Number.MAX_SAFE_INTEGER, 2);
    const histogram2 = new Int32Histogram(1, Number.MAX_SAFE_INTEGER, 2);
    histogram.recordValue(42);
    histogram2.recordValue(158);
    // when
    histogram.add(histogram2);
    // then
    expect(histogram.totalCount).toBe(2);
    expect(histogram.mean).toBe(100);
  });

  it("should add histograms of different sizes & precisions", () => {
    // given
    const histogram = build({
      lowestDiscernibleValue: 1,
      highestTrackableValue: 1024,
      autoResize: true,
      numberOfSignificantValueDigits: 2,
      bitBucketSize: "packed",
      useWebAssembly: true,
    });
    const histogram2 = build({
      lowestDiscernibleValue: 1,
      highestTrackableValue: 1024,
      autoResize: true,
      numberOfSignificantValueDigits: 3,
      bitBucketSize: 32,
      useWebAssembly: true,
    });
    //histogram2.autoResize = true;
    histogram.recordValue(42000);
    histogram2.recordValue(1000);
    // when
    histogram.add(histogram2);
    // then
    expect(histogram.totalCount).toBe(2);
    expect(Math.floor(histogram.mean / 100)).toBe(215);
  });

  it("should add histograms of different sizes", () => {
    // given
    const histogram = new Int32Histogram(1, Number.MAX_SAFE_INTEGER, 2);
    const histogram2 = new Int32Histogram(1, 1024, 2);
    histogram2.autoResize = true;
    histogram.recordValue(42000);
    histogram2.recordValue(1000);
    // when
    histogram.add(histogram2);
    // then
    expect(histogram.totalCount).toBe(2);
    expect(Math.floor(histogram.mean / 100)).toBe(215);
  });

  it("should be equal when another histogram of lower precision is added then subtracted", () => {
    // given
    const histogram = build({ numberOfSignificantValueDigits: 5 });
    const histogram2 = build({ numberOfSignificantValueDigits: 3 });
    histogram.recordValue(100);
    histogram2.recordValue(42000);
    // when
    const before = histogram.summary;
    histogram.add(histogram2);
    histogram.subtract(histogram2);
    // then
    expect(histogram.summary).toStrictEqual(before);
  });

  it("should update percentiles when another histogram of same characteristics is substracted", () => {
    // given
    const histogram = build({ numberOfSignificantValueDigits: 3 });
    const histogram2 = build({ numberOfSignificantValueDigits: 3 });
    histogram.recordValueWithCount(100, 2);
    histogram2.recordValueWithCount(100, 1);
    histogram.recordValueWithCount(200, 2);
    histogram2.recordValueWithCount(200, 1);
    histogram.recordValueWithCount(300, 2);
    histogram2.recordValueWithCount(300, 1);
    // when
    histogram.subtract(histogram2);
    // then
    expect(histogram.getValueAtPercentile(50)).toBe(200);
  });
});

describe("Histogram clearing support", () => {
  beforeAll(initWebAssembly);

  it("should reset data in order to reuse histogram", () => {
    // given
    const histogram = build({
      lowestDiscernibleValue: 1,
      highestTrackableValue: Number.MAX_SAFE_INTEGER,
      numberOfSignificantValueDigits: 5,
      useWebAssembly: true,
    });
    histogram.startTimeStampMsec = 42;
    histogram.endTimeStampMsec = 56;
    histogram.tag = "blabla";
    histogram.recordValue(1000);
    // when
    histogram.reset();
    // then
    expect(histogram.totalCount).toBe(0);
    expect(histogram.startTimeStampMsec).toBe(0);
    expect(histogram.endTimeStampMsec).toBe(0);
    expect(histogram.tag).toBe(NO_TAG);
    expect(histogram.maxValue).toBe(0);
    expect(histogram.minNonZeroValue).toBeGreaterThan(Number.MAX_SAFE_INTEGER);
    expect(histogram.getValueAtPercentile(99.999)).toBe(0);
  });

  it("should behave as new when reseted", () => {
    // given
    const histogram = build({
      lowestDiscernibleValue: 1,
      highestTrackableValue: 15000,
      numberOfSignificantValueDigits: 2,
    });
    const histogram2 = build({
      lowestDiscernibleValue: 1,
      highestTrackableValue: 15000,
      numberOfSignificantValueDigits: 2,
    });
    histogram.recordValue(1);
    histogram.recordValue(100);
    histogram.recordValue(2000);
    histogram.reset();
    // when
    histogram.recordValue(1000);
    histogram2.recordValue(1000);

    // then
    expect(histogram.mean).toBe(histogram2.mean);
  });
});
