import { describe, expect, it } from "vitest";

import { asEvaluatorFn } from "../../src/helpers/asEvaluatorFn";
import type { EvaluationResult } from "../../src/types";

type TestRecord = {
  input: string;
  output: string;
  [key: string]: unknown;
};

describe("asEvaluatorFn", () => {
  describe("synchronous functions", () => {
    it("should convert a sync function returning a number to an evaluator function", async () => {
      const fn = ({
        output,
        expected,
      }: {
        output: string;
        expected: string;
      }) => {
        return output === expected ? 1 : 0;
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "correct",
        expected: "correct",
      });

      expect(result).toEqual({ score: 1 });
    });

    it("should convert a sync function returning 0 to an evaluator function", async () => {
      const fn = ({
        output,
        expected,
      }: {
        output: string;
        expected: string;
      }) => {
        return output === expected ? 1 : 0;
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "wrong",
        expected: "correct",
      });

      expect(result).toEqual({ score: 0 });
    });

    it("should convert a sync function returning an EvaluationResult object", async () => {
      const fn = (_record: TestRecord): EvaluationResult => {
        return {
          score: 0.95,
          label: "high",
          explanation: "The output is of high quality",
        };
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({
        score: 0.95,
        label: "high",
        explanation: "The output is of high quality",
      });
    });

    it("should convert a sync function returning a string (label)", async () => {
      const fn = ({ output }: { output: string }) => {
        return output.length > 10 ? "long" : "short";
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "very long output text",
      });

      expect(result).toEqual({ label: "long" });
    });

    it("should convert a sync function returning an object with score only", async () => {
      const fn = (_record: TestRecord) => {
        return { score: 0.8 };
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({ score: 0.8 });
    });

    it("should convert a sync function returning an object with label only", async () => {
      const fn = (_record: TestRecord) => {
        return { label: "pass" };
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({ label: "pass" });
    });

    it("should convert a sync function returning an object with explanation only", async () => {
      const fn = (_record: TestRecord) => {
        return { explanation: "This is a test explanation" };
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({ explanation: "This is a test explanation" });
    });

    it("should handle a sync function returning null", async () => {
      const fn = () => null;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({});
    });

    it("should handle a sync function returning undefined", async () => {
      const fn = () => undefined;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({});
    });

    it("should handle a sync function returning an empty object", async () => {
      const fn = () => ({});

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({});
    });
  });

  describe("asynchronous functions", () => {
    it("should convert an async function returning a number to an evaluator function", async () => {
      const fn = async ({
        output,
        expected,
      }: {
        output: string;
        expected: string;
      }) => {
        await new Promise((resolve) => setTimeout(resolve, 10));
        return output === expected ? 1 : 0;
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "correct",
        expected: "correct",
      });

      expect(result).toEqual({ score: 1 });
    });

    it("should convert an async function returning an EvaluationResult object", async () => {
      const fn = async (_record: TestRecord): Promise<EvaluationResult> => {
        await new Promise((resolve) => setTimeout(resolve, 10));
        return {
          score: 0.85,
          label: "medium",
          explanation: "The output quality is medium",
        };
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({
        score: 0.85,
        label: "medium",
        explanation: "The output quality is medium",
      });
    });

    it("should convert an async function returning a string (label)", async () => {
      const fn = async ({ output }: { output: string }) => {
        await new Promise((resolve) => setTimeout(resolve, 10));
        return output.length > 5 ? "long" : "short";
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "very long output",
      });

      expect(result).toEqual({ label: "long" });
    });

    it("should handle an async function that throws an error", async () => {
      const fn = async () => {
        throw new Error("Test error");
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      await expect(
        evaluatorFn({
          input: "test",
          output: "some output",
        })
      ).rejects.toThrow("Test error");
    });
  });

  describe("function arguments", () => {
    it("should pass the record argument to the original function", async () => {
      const fn = (record: TestRecord) => {
        return record.output === "expected" ? 1 : 0;
      };

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result1 = await evaluatorFn({
        input: "test",
        output: "expected",
      });

      expect(result1).toEqual({ score: 1 });

      const result2 = await evaluatorFn({
        input: "test",
        output: "unexpected",
      });

      expect(result2).toEqual({ score: 0 });
    });

    it("should handle functions with no parameters", async () => {
      const fn = () => 42;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({ score: 42 });
    });
  });

  describe("edge cases", () => {
    it("should handle a function returning a negative number", async () => {
      const fn = () => -1;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({ score: -1 });
    });

    it("should handle a function returning a floating point number", async () => {
      const fn = () => 0.123456789;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({ score: 0.123456789 });
    });

    it("should handle a function returning Infinity", async () => {
      const fn = () => Infinity;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({ score: Infinity });
    });

    it("should handle a function returning NaN", async () => {
      const fn = () => NaN;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({ score: NaN });
    });

    it("should handle a function returning an object with all EvaluationResult fields", async () => {
      const fn = () => ({
        score: 0.9,
        label: "excellent",
        explanation: "Perfect output",
      });

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({
        score: 0.9,
        label: "excellent",
        explanation: "Perfect output",
      });
    });

    it("should handle a function returning an object with extra fields", async () => {
      const fn = () => ({
        score: 0.8,
        label: "good",
        extraField: "this should be ignored",
        anotherField: 123,
      });

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      // Only EvaluationResult fields should be included
      expect(result).toEqual({
        score: 0.8,
        label: "good",
      });
      expect(result).not.toHaveProperty("extraField");
      expect(result).not.toHaveProperty("anotherField");
    });

    it("should handle a function returning a boolean (converted to empty result)", async () => {
      const fn = () => true;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({});
    });

    it("should handle a function returning an array (converted to empty result)", async () => {
      const fn = () => [1, 2, 3];

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toEqual({});
    });
  });

  describe("return type", () => {
    it("should always return a Promise", () => {
      const fn = () => 1;

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toBeInstanceOf(Promise);
    });

    it("should return a Promise that resolves to EvaluationResult", async () => {
      const fn = () => ({ score: 0.5 });

      const evaluatorFn = asEvaluatorFn<TestRecord>(fn);

      const result = await evaluatorFn({
        input: "test",
        output: "some output",
      });

      expect(result).toHaveProperty("score");
      expect(typeof result.score).toBe("number");
    });
  });
});
