import type { Selectable, Transaction } from "@tailor-platform/sdk/kysely";
import type { Generated, Timestamp } from "@tailor-platform/sdk/kysely";
import { describe, it, expect, expectTypeOf } from "vitest";
import { defineLifecycle, executeTransition, type TransitionErrors } from "./defineLifecycle";

describe("defineLifecycle", () => {
  describe("single-from transitions", () => {
    const productLifecycle = defineLifecycle({
      activate: { from: ["DRAFT"], to: "ACTIVE" },
      deactivate: { from: ["ACTIVE"], to: "ARCHIVED" },
      reactivate: { from: ["ARCHIVED"], to: "ACTIVE" },
    });

    it("derives states from transitions", () => {
      expect(productLifecycle.states).toEqual(["DRAFT", "ACTIVE", "ARCHIVED"]);
    });

    it("tryTransition returns the target state for valid transitions", () => {
      expect(productLifecycle.tryTransition("DRAFT", "activate")).toBe("ACTIVE");
      expect(productLifecycle.tryTransition("ACTIVE", "deactivate")).toBe("ARCHIVED");
      expect(productLifecycle.tryTransition("ARCHIVED", "reactivate")).toBe("ACTIVE");
    });

    it("tryTransition returns undefined for invalid transitions", () => {
      expect(productLifecycle.tryTransition("ACTIVE", "activate")).toBeUndefined();
      expect(productLifecycle.tryTransition("DRAFT", "deactivate")).toBeUndefined();
      expect(productLifecycle.tryTransition("DRAFT", "reactivate")).toBeUndefined();
    });

    it("tryTransition narrows the returned state to the transition's target literal", () => {
      const result = productLifecycle.tryTransition("DRAFT", "activate");
      expectTypeOf(result).toEqualTypeOf<"ACTIVE" | undefined>();
    });

    it("infers literal state types", () => {
      type State = (typeof productLifecycle.states)[number];
      expectTypeOf<State>().toEqualTypeOf<"DRAFT" | "ACTIVE" | "ARCHIVED">();
    });

    it("infers literal transition types", () => {
      type Transition = Parameters<typeof productLifecycle.tryTransition>[1];
      expectTypeOf<Transition>().toEqualTypeOf<"activate" | "deactivate" | "reactivate">();
    });
  });

  describe("multi-from transitions", () => {
    const poLifecycle = defineLifecycle({
      submit: { from: ["DRAFT"], to: "SUBMITTED" },
      approve: { from: ["SUBMITTED"], to: "ORDERED" },
      reject: { from: ["SUBMITTED"], to: "DRAFT" },
      cancel: { from: ["DRAFT", "SUBMITTED", "ORDERED"], to: "CANCELLED" },
      close: { from: ["ORDERED"], to: "CLOSED" },
    });

    it("derives all states including terminal ones", () => {
      expect(poLifecycle.states).toContain("DRAFT");
      expect(poLifecycle.states).toContain("SUBMITTED");
      expect(poLifecycle.states).toContain("ORDERED");
      expect(poLifecycle.states).toContain("CANCELLED");
      expect(poLifecycle.states).toContain("CLOSED");
      expect(poLifecycle.states).toHaveLength(5);
    });

    it("tryTransition returns the target state for valid transitions", () => {
      expect(poLifecycle.tryTransition("DRAFT", "submit")).toBe("SUBMITTED");
      expect(poLifecycle.tryTransition("DRAFT", "cancel")).toBe("CANCELLED");
      expect(poLifecycle.tryTransition("SUBMITTED", "cancel")).toBe("CANCELLED");
      expect(poLifecycle.tryTransition("ORDERED", "cancel")).toBe("CANCELLED");
    });

    it("tryTransition returns undefined for invalid transitions", () => {
      expect(poLifecycle.tryTransition("CANCELLED", "cancel")).toBeUndefined();
      expect(poLifecycle.tryTransition("CLOSED", "cancel")).toBeUndefined();
      expect(poLifecycle.tryTransition("DRAFT", "approve")).toBeUndefined();
    });

    it("infers literal state types", () => {
      type State = (typeof poLifecycle.states)[number];
      expectTypeOf<State>().toEqualTypeOf<
        "DRAFT" | "SUBMITTED" | "ORDERED" | "CANCELLED" | "CLOSED"
      >();
    });

    it("infers literal transition types", () => {
      type Transition = Parameters<typeof poLifecycle.tryTransition>[1];
      expectTypeOf<Transition>().toEqualTypeOf<
        "submit" | "approve" | "reject" | "cancel" | "close"
      >();
    });
  });
});

describe("executeTransition", () => {
  class TestNotFoundError extends Error {
    constructor(id: string) {
      super(`Not found: ${id}`);
    }
  }
  class TestInvalidTransitionError extends Error {
    constructor(id: string) {
      super(`Invalid transition: ${id}`);
    }
  }
  const testErrors: TransitionErrors<TestNotFoundError, TestInvalidTransitionError> = {
    notFound: TestNotFoundError,
    invalidTransition: TestInvalidTransitionError,
  };

  type DB = {
    Product: {
      id: Generated<string>;
      name: string;
      status: "DRAFT" | "ACTIVE" | "ARCHIVED";
      createdAt: Generated<Timestamp>;
    };
    Order: {
      id: Generated<string>;
      orderStatus: "OPEN" | "CLOSED";
      createdAt: Generated<Timestamp>;
    };
  };

  const productLifecycle = defineLifecycle({
    activate: { from: ["DRAFT"], to: "ACTIVE" },
    deactivate: { from: ["ACTIVE"], to: "ARCHIVED" },
  });

  const orderLifecycle = defineLifecycle({
    close: { from: ["OPEN"], to: "CLOSED" },
  });

  it("accepts valid table, field, and lifecycle combinations", () => {
    let db!: Transaction<DB>;
    () =>
      executeTransition({
        db,
        tableName: "Product",
        statusField: "status",
        id: "1",
        transition: "activate",
        lifecycle: productLifecycle,
        errors: testErrors,
      });
    () =>
      executeTransition({
        db,
        tableName: "Order",
        statusField: "orderStatus",
        id: "1",
        transition: "close",
        lifecycle: orderLifecycle,
        errors: testErrors,
      });
  });

  it("rejects invalid table names", () => {
    let db!: Transaction<DB>;
    () =>
      executeTransition({
        db,
        // @ts-expect-error -- "BadTable" is not a key of DB
        tableName: "BadTable",
        // @ts-expect-error -- cascading: "BadTable" is not a key of DB
        statusField: "status",
        id: "1",
        transition: "activate",
        // @ts-expect-error -- cascading: "BadTable" is not a key of DB
        lifecycle: productLifecycle,
        errors: testErrors,
      });
  });

  it("rejects invalid field names", () => {
    let db!: Transaction<DB>;
    () =>
      executeTransition({
        db,
        tableName: "Product",
        // @ts-expect-error -- "orderStatus" is not a column of Product
        statusField: "orderStatus",
        id: "1",
        transition: "activate",
        lifecycle: productLifecycle,
        errors: testErrors,
      });
  });

  it("rejects lifecycle whose states do not match the column type", () => {
    let db!: Transaction<DB>;
    () =>
      executeTransition({
        db,
        tableName: "Order",
        statusField: "orderStatus",
        id: "1",
        transition: "activate",
        // @ts-expect-error -- productLifecycle states don't match Order.orderStatus
        lifecycle: productLifecycle,
        errors: testErrors,
      });
  });

  it("rejects transition names not defined in the lifecycle", () => {
    let db!: Transaction<DB>;
    () =>
      executeTransition({
        db,
        tableName: "Order",
        statusField: "orderStatus",
        id: "1",
        // @ts-expect-error -- "activate" is not a transition in orderLifecycle
        transition: "activate",
        lifecycle: orderLifecycle,
        errors: testErrors,
      });
  });

  it("narrows result type to specific value and error classes", () => {
    let db!: Transaction<DB>;
    async () => {
      const result = await executeTransition({
        db,
        tableName: "Product",
        statusField: "status",
        id: "1",
        transition: "activate",
        lifecycle: productLifecycle,
        errors: testErrors,
      });
      if (result.ok) {
        expectTypeOf(result.value).toEqualTypeOf<Selectable<DB["Product"]>>();
      } else {
        expectTypeOf(result.error).toEqualTypeOf<TestNotFoundError | TestInvalidTransitionError>();
      }
    };
  });
});
