import {
  checkTraceExistsAndGetTimestamp,
  createTracesCh,
} from "@langfuse/shared/src/server";
import {
  getTraceById,
  getTracesBySessionId,
} from "@langfuse/shared/src/server";
import { v4 } from "uuid";
import { createObservation, createTrace } from "@langfuse/shared/src/server";
import { createObservationsCh } from "@langfuse/shared/src/server";

const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";

describe("Clickhouse Traces Repository Test", () => {
  it("should throw if no traces are found", async () => {
    expect(
      await getTraceById({ traceId: v4(), projectId: v4() }),
    ).toBeUndefined();
  });

  it("should return a trace if it exists", async () => {
    const traceId = v4();

    const trace = createTrace({
      id: traceId,
      project_id: projectId,
      session_id: v4(),
      timestamp: Date.now(),
      metadata: {},
      public: false,
      bookmarked: false,
      name: "Test Trace",
      tags: [],
      release: null,
      version: null,
      user_id: null,
      input: JSON.stringify({
        this: {
          is: {
            a: ["complex", "object"],
          },
        },
      }),
      output: "regular string",
      created_at: Date.now(),
      updated_at: Date.now(),
      event_ts: Date.now(),
      is_deleted: 0,
    });

    await createTracesCh([trace]);

    const result = await getTraceById({
      traceId,
      projectId,
      timestamp: new Date(trace.timestamp),
    });
    expect(result).not.toBeNull();
    if (!result) {
      return;
    }
    expect(result.id).toEqual(trace.id);
    expect(result.projectId).toEqual(trace.project_id);
    expect(result.name).toEqual(trace.name);
    expect(result.timestamp).toEqual(new Date(trace.timestamp));
    expect(result.tags).toEqual(trace.tags);
    expect(result.bookmarked).toEqual(trace.bookmarked);
    expect(result.release).toEqual(trace.release);
    expect(result.version).toEqual(trace.version);
    expect(result.userId).toEqual(trace.user_id);
    expect(result.sessionId).toEqual(trace.session_id);
    expect(result.public).toEqual(trace.public);
    expect(result.input).toEqual(JSON.parse(trace.input ?? "{}"));
    expect(result.output).toEqual("regular string");
    expect(result.metadata).toEqual(trace.metadata);
    expect(result.createdAt.getTime()).toBeCloseTo(
      new Date(trace.created_at).getTime(),
      -2, // Up to 50ms precision
    );
    expect(result.updatedAt.getTime()).toBeCloseTo(
      new Date(trace.updated_at).getTime(),
      -2, // Up to 50ms precision
    );
  });

  it("should find a trace if no timestamp is provided", async () => {
    const traceId = v4();

    const trace = createTrace({
      id: traceId,
      project_id: projectId,
      session_id: v4(),
      timestamp: Date.now(),
      metadata: {},
      public: false,
      bookmarked: false,
      name: "Test Trace",
      tags: [],
      release: null,
      version: null,
      user_id: null,
      created_at: Date.now(),
      updated_at: Date.now(),
      event_ts: Date.now(),
      is_deleted: 0,
    });

    await createTracesCh([trace]);

    const result = await getTraceById({ traceId, projectId });
    expect(result).not.toBeNull();
    if (!result) {
      return;
    }
    expect(result.id).toEqual(trace.id);
    expect(result.projectId).toEqual(trace.project_id);
    expect(result.name).toEqual(trace.name);
    expect(result.timestamp).toEqual(new Date(trace.timestamp));
    expect(result.tags).toEqual(trace.tags);
    expect(result.bookmarked).toEqual(trace.bookmarked);
    expect(result.release).toEqual(trace.release);
    expect(result.version).toEqual(trace.version);
    expect(result.userId).toEqual(trace.user_id);
    expect(result.sessionId).toEqual(trace.session_id);
    expect(result.public).toEqual(trace.public);
    expect(result.input).toEqual(null);
    expect(result.output).toEqual(null);
    expect(result.metadata).toEqual(trace.metadata);
    expect(result.createdAt.getTime()).toBeCloseTo(
      new Date(trace.created_at).getTime(),
      -2, // Up to 50ms precision
    );
    expect(result.updatedAt.getTime()).toBeCloseTo(
      new Date(trace.updated_at).getTime(),
      -2, // Up to 50ms precision
    );
  });

  it("should retrieve traces by session ID", async () => {
    const sessionId = v4();
    const trace1 = createTrace({
      id: v4(),
      project_id: projectId,
      session_id: sessionId,
      timestamp: Date.now(),
      metadata: {},
      public: false,
      bookmarked: false,
      name: "Trace 1",
      tags: [],
      release: null,
      version: null,
      user_id: null,
      created_at: Date.now(),
      updated_at: Date.now(),
      event_ts: Date.now(),
      is_deleted: 0,
    });

    const trace2 = createTrace({
      id: v4(),
      project_id: projectId,
      session_id: sessionId,
      timestamp: Date.now(),
      metadata: {},
      public: false,
      bookmarked: false,
      name: "Trace 2",
      tags: [],
      release: null,
      version: null,
      user_id: null,
      created_at: Date.now(),
      updated_at: Date.now(),
      event_ts: Date.now(),
      is_deleted: 0,
    });

    await createTracesCh([trace1, trace2]);

    const results = await getTracesBySessionId(projectId, [sessionId]);
    expect(results).toHaveLength(2);

    const resultIds = results.map((result) => result.id);
    expect(resultIds).toContain(trace1.id);
    expect(resultIds).toContain(trace2.id);
  });

  it("should check if trace exists with level filter", async () => {
    const traceId = v4();
    const trace = createTrace({
      id: traceId,
      user_id: "user-1",
      project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
      metadata: { key: "value" },
      release: "1.0.0",
      version: "2.0.0",
    });

    const observations = [
      createObservation({
        trace_id: trace.id,
        project_id: trace.project_id,
        name: "observation-name",
        end_time: new Date().getTime(),
        start_time: new Date().getTime() - 1000,
        input: "input",
        output: "output",
        provided_model_name: "model-1",
        level: "ERROR",
      }),
      createObservation({
        trace_id: trace.id,
        project_id: trace.project_id,
        name: "observation-name-2",
        end_time: new Date().getTime(),
        start_time: new Date().getTime() - 100000,
        input: "input-2",
        output: "output-2",
        provided_model_name: "model-2",
      }),
    ];

    await createTracesCh([trace]);
    await createObservationsCh(observations);

    const { exists } = await checkTraceExistsAndGetTimestamp({
      projectId,
      traceId,
      timestamp: new Date(),
      filter: [
        {
          type: "stringOptions",
          column: "level",
          operator: "any of",
          value: ["ERROR"],
        },
      ],
      maxTimeStamp: undefined,
    });
    expect(exists).toBe(true);
  });

  it("should check if trace exists with level filter none of", async () => {
    const traceId = v4();
    const trace = createTrace({
      id: traceId,
      user_id: "user-1",
      project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
      metadata: { key: "value" },
      release: "1.0.0",
      version: "2.0.0",
    });

    const observations = [
      createObservation({
        trace_id: trace.id,
        project_id: trace.project_id,
        name: "observation-name",
        end_time: new Date().getTime(),
        start_time: new Date().getTime() - 1000,
        input: "input",
        output: "output",
        provided_model_name: "model-1",
        level: "ERROR",
      }),
      createObservation({
        trace_id: trace.id,
        project_id: trace.project_id,
        name: "observation-name-2",
        end_time: new Date().getTime(),
        start_time: new Date().getTime() - 100000,
        input: "input-2",
        output: "output-2",
        provided_model_name: "model-2",
      }),
    ];

    await createTracesCh([trace]);
    await createObservationsCh(observations);

    const { exists } = await checkTraceExistsAndGetTimestamp({
      projectId,
      traceId,
      timestamp: new Date(),
      filter: [
        {
          type: "stringOptions",
          column: "level",
          operator: "none of",
          value: ["ERROR"],
        },
      ],
      maxTimeStamp: undefined,
    });
    expect(exists).toBe(false);
  });
  it("should check if trace exists without level filter", async () => {
    const traceId = v4();
    const trace = createTrace({
      id: traceId,
      user_id: "user-1",
      project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
      metadata: { key: "value" },
      release: "1.0.0",
      version: "2.0.0",
    });

    const observations = [
      createObservation({
        trace_id: trace.id,
        project_id: trace.project_id,
        name: "observation-name",
        end_time: new Date().getTime(),
        start_time: new Date().getTime() - 1000,
        input: "input",
        output: "output",
        provided_model_name: "model-1",
        level: "ERROR",
      }),
      createObservation({
        trace_id: trace.id,
        project_id: trace.project_id,
        name: "observation-name-2",
        end_time: new Date().getTime(),
        start_time: new Date().getTime() - 100000,
        input: "input-2",
        output: "output-2",
        provided_model_name: "model-2",
      }),
    ];

    await createTracesCh([trace]);
    await createObservationsCh(observations);

    const { exists } = await checkTraceExistsAndGetTimestamp({
      projectId,
      traceId,
      timestamp: new Date(),
      filter: [],
      maxTimeStamp: undefined,
    });
    expect(exists).toBe(true);
  });
  it("should check if trace exists with error level count > 0", async () => {
    const traceId = v4();
    const trace = createTrace({
      id: traceId,
      user_id: "user-1",
      project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
      metadata: { key: "value" },
      release: "1.0.0",
      version: "2.0.0",
    });

    const observations = [
      createObservation({
        trace_id: trace.id,
        project_id: trace.project_id,
        name: "observation-name",
        end_time: new Date().getTime(),
        start_time: new Date().getTime() - 1000,
        input: "input",
        output: "output",
        provided_model_name: "model-1",
        level: "ERROR",
      }),
      createObservation({
        trace_id: trace.id,
        project_id: trace.project_id,
        name: "observation-name-2",
        end_time: new Date().getTime(),
        start_time: new Date().getTime() - 100000,
        input: "input-2",
        output: "output-2",
        provided_model_name: "model-2",
      }),
    ];

    await createTracesCh([trace]);
    await createObservationsCh(observations);

    const { exists } = await checkTraceExistsAndGetTimestamp({
      projectId,
      traceId,
      timestamp: new Date(),
      filter: [
        {
          type: "number",
          column: "errorCount",
          operator: ">",
          value: 0,
        },
      ],
      maxTimeStamp: undefined,
    });
    expect(exists).toBe(true);
  });

  it("should handle timestamp filter in checkTraceExistsAndGetTimestamp", async () => {
    const traceId = v4();
    const trace = createTrace({
      id: traceId,
      user_id: "user-1",
      project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
      metadata: { key: "value" },
      release: "1.0.0",
      version: "2.0.0",
      timestamp: Date.now(),
    });

    await createTracesCh([trace]);

    const { exists } = await checkTraceExistsAndGetTimestamp({
      projectId,
      traceId,
      timestamp: new Date(),
      filter: [
        {
          type: "datetime",
          column: "timestamp",
          operator: ">=",
          value: new Date(Date.now() - 3600000),
        },
      ],
      maxTimeStamp: undefined,
    });
    expect(exists).toBe(true);
  });
});
