/*!
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import dayjs from "dayjs";
import dayjsDuration from "dayjs/plugin/duration";
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";

import {
  getDuration,
  getDurationTickStep,
  humanizeSeconds,
  renderCompactDuration,
  renderDuration,
  getRelativeTime,
} from "./datetimeUtils";

dayjs.extend(dayjsDuration);

describe("getDuration & formatDuration", () => {
  it("handles durations less than 60 seconds", () => {
    const start = "2024-03-14T10:00:00.000Z";
    const end = "2024-03-14T10:00:05.5111111Z";

    expect(getDuration(start, end)).toBe("00:00:05.511");
  });

  it("handles durations spanning multiple days", () => {
    const start = "2024-03-14T10:00:00.000Z";
    const end = "2024-03-17T15:30:45.000Z";

    expect(getDuration(start, end)).toBe("3d05:30:45");
  });

  it("handles exactly 24 hours", () => {
    const start = "2024-03-14T10:00:00.000Z";
    const end = "2024-03-15T10:00:00.000Z";

    expect(getDuration(start, end)).toBe("1d00:00:00");
  });

  it("handles hours and minutes without days", () => {
    const start = "2024-03-14T10:00:00.000Z";
    const end = "2024-03-14T12:30:00.000Z";

    expect(getDuration(start, end)).toBe("02:30:00");
  });

  it("omits milliseconds when withMilliseconds is false", () => {
    const start = "2024-03-14T10:00:00.000Z";
    const end = "2024-03-14T10:00:05.511Z";

    expect(getDuration(start, end, false)).toBe("00:00:05");
  });

  it("handles small, null or undefined values", () => {
    expect(getDuration(null, null)).toBe(undefined);
    expect(getDuration(undefined, undefined)).toBe(undefined);
    expect(getDuration(null, "2024-03-14T10:00:10.000Z")).toBe(undefined);
    expect(renderDuration(0.00001)).toBe(undefined);
  });

  it("falls back to current time when endDate is null (running task)", () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2024-03-14T10:00:10.000Z"));

    const start = "2024-03-14T10:00:00.000Z";

    expect(getDuration(start, null)).toBe("00:00:10");
    expect(getDuration(start, undefined)).toBe("00:00:10");

    vi.useRealTimers();
  });

  it("handles both numbers and duration objects", () => {
    expect(renderDuration(dayjs.duration(10, "seconds"))).toBe("00:00:10");
    expect(renderDuration(10)).toBe("00:00:10");
  });

  it("handles floating point milliseconds", () => {
    expect(renderDuration(dayjs.duration(10.000499738, "seconds"))).toBe("00:00:10");
    expect(renderDuration(10.000499738)).toBe("00:00:10");
    expect(renderDuration(dayjs.duration(10.0005, "seconds"))).toBe("00:00:10.001");
    expect(renderDuration(10.0005)).toBe("00:00:10.001");
    expect(renderDuration(dayjs.duration(10.838999738, "seconds"))).toBe("00:00:10.839");
    expect(renderDuration(10.838999738)).toBe("00:00:10.839");
  });
});

describe("getRelativeTime", () => {
  const fixedNow = new Date("2024-03-14T10:00:10.000Z");

  beforeAll(() => {
    vi.useFakeTimers();
    vi.setSystemTime(fixedNow);
  });

  afterAll(() => {
    vi.useRealTimers();
  });

  it("returns relative time for a valid date", () => {
    const date = "2024-03-14T10:00:00.000Z";

    expect(getRelativeTime(date)).toBe("a few seconds ago");
  });

  it("returns an empty string for undefined dates", () => {
    expect(getRelativeTime(undefined)).toBe("");
  });

  it("handles future dates", () => {
    const futureDate = "2024-03-14T10:00:20.000Z";

    expect(getRelativeTime(futureDate)).toBe("in a few seconds");
  });
});

describe("renderCompactDuration", () => {
  it.each([
    [0, "0s"],
    [-5, "0s"],
    [Number.NaN, "0s"],
    [Number.POSITIVE_INFINITY, "0s"],
    [0.25, "250ms"],
    [45, "45s"],
    [540, "9m"],
    [545, "9m 5s"],
    [3600, "1h"],
    [5400, "1h 30m"],
    [86_400, "1d"],
    [102_600, "1d 4h"],
  ])("formats %s seconds as %s", (seconds, expected) => {
    expect(renderCompactDuration(seconds)).toBe(expected);
  });
});

describe("getDurationTickStep", () => {
  it.each([
    [0, 1],
    [-1, 1],
    [Number.NaN, 1],
    [8, 1],
    [45, 10],
    [300, 60],
    [2000, 300],
    [36_000, 7200],
  ])("picks a %s second range step of %s seconds", (maxSeconds, expected) => {
    expect(getDurationTickStep(maxSeconds)).toBe(expected);
  });

  it("keeps the tick count within the requested budget", () => {
    expect(getDurationTickStep(2000) * 8).toBeGreaterThanOrEqual(2000);
  });

  it("falls back to an even split beyond the largest known step", () => {
    expect(getDurationTickStep(10_000_000)).toBe(1_250_000);
  });
});

describe("humanizeSeconds", () => {
  it.each([
    [3600, "an hour"],
    [86_400, "a day"],
  ])("humanizes %s seconds as %s", (seconds, expected) => {
    expect(humanizeSeconds(seconds)).toBe(expected);
  });

  it.each([[null], [undefined], [Number.NaN], [Number.POSITIVE_INFINITY]])(
    "returns undefined without a finite interval (%s)",
    (seconds) => {
      expect(humanizeSeconds(seconds)).toBeUndefined();
    },
  );
});
