import { describe, test, expect } from "vitest";
import type { SpanEvent } from "@trigger.dev/core/v3";
import { createTimelineSpanEventsFromSpanEvents } from "../app/utils/timelineSpanEvents";
import { millisecondsToNanoseconds } from "@trigger.dev/core/v3/utils/durations";

describe("createTimelineSpanEventsFromSpanEvents", () => {
  const sampleSpanEvents: SpanEvent[] = [
    {
      name: "trigger.dev/start",
      time: new Date("2025-04-04T08:39:27.046Z"),
      properties: { event: "fork", duration: 127 },
    },
    {
      name: "trigger.dev/start",
      time: new Date("2025-04-04T08:39:26.985Z"),
      properties: { event: "create_attempt", duration: 56 },
    },
    {
      name: "trigger.dev/start",
      time: new Date("2025-04-04T08:39:26.980Z"),
      properties: { event: "dequeue", duration: 0 },
    },
    {
      name: "trigger.dev/start",
      time: new Date("2025-04-04T08:39:27.224Z"),
      properties: {
        file: "src/trigger/chat.ts",
        event: "import",
        duration: 67,
        entryPoint: "/project/.trigger/tmp/build-AL7zTl/src/trigger/chat.mjs",
      },
    },
  ];

  // Sample events without fork event
  const eventsWithoutFork: SpanEvent[] = [
    {
      name: "trigger.dev/start",
      time: new Date("2025-04-04T08:39:26.980Z"),
      properties: { event: "dequeue", duration: 0 },
    },
    {
      name: "trigger.dev/start",
      time: new Date("2025-04-04T08:39:26.985Z"),
      properties: { event: "create_attempt", duration: 56 },
    },
    {
      name: "trigger.dev/start",
      time: new Date("2025-04-04T08:39:27.224Z"),
      properties: {
        file: "src/trigger/chat.ts",
        event: "import",
        duration: 67,
        entryPoint: "/project/.trigger/tmp/build-AL7zTl/src/trigger/chat.mjs",
      },
    },
  ];

  test("should filter non-admin events when isAdmin is false", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, false);

    // Only dequeue and fork events should be visible for non-admins
    expect(result.length).toBe(3);
    expect(result.some((event) => event.name === "Dequeued")).toBe(true);
    expect(result.some((event) => event.name === "Launched")).toBe(true);
    expect(result.some((event) => event.name === "Attempt created")).toBe(false);
    expect(result.some((event) => event.name.includes("Importing"))).toBe(true);
  });

  test("should include all events when isAdmin is true", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);

    expect(result.length).toBe(4);
    expect(result.some((event) => event.name === "Dequeued")).toBe(true);
    expect(result.some((event) => event.name === "Launched")).toBe(true);
    expect(result.some((event) => event.name === "Attempt created")).toBe(true);
    expect(result.some((event) => event.name === "Importing task file")).toBe(true);
  });

  test("should sort events by timestamp", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);

    // Events should be sorted by time (offset)
    expect(result[0].name).toBe("Dequeued");
    expect(result[1].name).toBe("Attempt created");
    expect(result[2].name).toBe("Launched");
    expect(result[3].name).toBe("Importing task file");
  });

  test("should calculate offsets correctly from the first event", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);

    // First event (dequeue) should have offset 0
    const firstEventTime = new Date("2025-04-04T08:39:26.980Z").getTime();

    expect(result[0].offset).toBe(0);
    expect(result[1].offset).toBe(
      millisecondsToNanoseconds(new Date("2025-04-04T08:39:26.985Z").getTime() - firstEventTime)
    );
    expect(result[2].offset).toBe(
      millisecondsToNanoseconds(new Date("2025-04-04T08:39:27.046Z").getTime() - firstEventTime)
    );
    expect(result[3].offset).toBe(
      millisecondsToNanoseconds(new Date("2025-04-04T08:39:27.224Z").getTime() - firstEventTime)
    );
  });

  test("should use the provided relativeStartTime when specified", () => {
    const customStartTime = new Date("2025-04-04T08:39:26.900Z").getTime();
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true, customStartTime);

    // Offsets should be calculated relative to customStartTime
    expect(result[0].offset).toBe(
      millisecondsToNanoseconds(new Date("2025-04-04T08:39:26.980Z").getTime() - customStartTime)
    );
    expect(result[1].offset).toBe(
      millisecondsToNanoseconds(new Date("2025-04-04T08:39:26.985Z").getTime() - customStartTime)
    );
    expect(result[2].offset).toBe(
      millisecondsToNanoseconds(new Date("2025-04-04T08:39:27.046Z").getTime() - customStartTime)
    );
    expect(result[3].offset).toBe(
      millisecondsToNanoseconds(new Date("2025-04-04T08:39:27.224Z").getTime() - customStartTime)
    );
  });

  test("should handle empty span events array", () => {
    const result = createTimelineSpanEventsFromSpanEvents([], true);
    expect(result).toEqual([]);
  });

  test("should handle undefined span events", () => {
    const result = createTimelineSpanEventsFromSpanEvents(
      undefined as unknown as SpanEvent[],
      true
    );
    expect(result).toEqual([]);
  });

  test("should handle non-matching span events", () => {
    const nonMatchingEvents: SpanEvent[] = [
      {
        name: "non-trigger.dev/event",
        time: new Date("2025-04-04T08:39:27.046Z"),
        properties: { event: "something", duration: 127 },
      },
    ];

    const result = createTimelineSpanEventsFromSpanEvents(nonMatchingEvents, true);
    expect(result).toEqual([]);
  });

  test("should set marker variant correctly", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);

    // First event should have start-cap marker
    expect(result[0].markerVariant).toBe("start-cap");

    // Other events should have dot-hollow marker
    for (let i = 1; i < result.length; i++) {
      expect(result[i].markerVariant).toBe("dot-hollow");
    }
  });

  test("should include helpText for known events", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);

    expect(result.find((e) => e.name === "Dequeued")?.helpText).toBe(
      "The run was dequeued from the queue"
    );
    expect(result.find((e) => e.name === "Launched")?.helpText).toBe(
      "The process was created to run the task"
    );
    expect(result.find((e) => e.name === "Attempt created")?.helpText).toBe(
      "An attempt was created for the run"
    );
    expect(result.find((e) => e.name === "Importing task file")?.helpText).toBe(
      "A task file was imported"
    );
  });

  test("should not attach raw span event properties to timeline events", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);

    expect(result.length).toBeGreaterThan(0);
    for (const event of result) {
      expect(event).not.toHaveProperty("properties");
    }
  });

  test("should preserve duration from span events", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);

    expect(result.find((e) => e.name === "Dequeued")?.duration).toBe(0);
    expect(result.find((e) => e.name === "Launched")?.duration).toBe(127);
    expect(result.find((e) => e.name === "Attempt created")?.duration).toBe(56);
    expect(result.find((e) => e.name === "Importing task file")?.duration).toBe(67);
  });

  test("should use fallback name for import event without file property", () => {
    const eventsWithoutFile: SpanEvent[] = [
      {
        name: "trigger.dev/start",
        time: new Date("2025-04-04T08:39:27.224Z"),
        properties: {
          event: "import",
          duration: 67,
        },
      },
    ];

    const result = createTimelineSpanEventsFromSpanEvents(eventsWithoutFile, true);

    expect(result.length).toBe(1);
    expect(result[0].name).toBe("Importing task file");
  });

  test("should include import events for non-admin when no fork event exists", () => {
    const result = createTimelineSpanEventsFromSpanEvents(eventsWithoutFork, false);

    // Without fork event, import should also be visible for non-admins
    expect(result.length).toBe(2);
    expect(result.some((event) => event.name === "Dequeued")).toBe(true);
    expect(result.some((event) => event.name === "Importing task file")).toBe(true);

    // create_attempt should still be admin-only
    expect(result.some((event) => event.name === "Attempt created")).toBe(false);
  });

  test.skip("should filter import events for non-admin when fork event exists", () => {
    const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, false);

    // With fork event, import should be hidden for non-admins
    expect(result.length).toBe(2);
    expect(result.some((event) => event.name === "Dequeued")).toBe(true);
    expect(result.some((event) => event.name === "Launched")).toBe(true);
    expect(result.some((event) => event.name.includes("Importing"))).toBe(false);
  });
});
