import { vi, expect, type Mock } from "vitest";
import type { Transaction } from "@tailor-platform/sdk/kysely";
import {
  InsufficientPermissionError,
  type CommandContext,
  type Command,
  type Result,
  type ErrorOf,
  type ValueOf,
} from "../shared";

export type Spy = Mock;

export interface QueryBuilder {
  where(...args: unknown[]): QueryBuilder;
  selectAll(...args: unknown[]): QueryBuilder;
  select(...args: unknown[]): QueryBuilder;
  innerJoin(...args: unknown[]): QueryBuilder;
  values(...args: unknown[]): QueryBuilder;
  returningAll(...args: unknown[]): QueryBuilder;
  set(...args: unknown[]): QueryBuilder;
  orderBy(...args: unknown[]): QueryBuilder;
  limit(...args: unknown[]): QueryBuilder;
  offset(...args: unknown[]): QueryBuilder;
  forUpdate(): QueryBuilder;
  executeTakeFirst(): unknown;
  executeTakeFirstOrThrow(): unknown;
  execute(): unknown;
}

export interface MockDbSpies {
  select: Spy;
  update: Spy;
  insert: Spy;
  delete: Spy;
  values: Spy;
  set: Spy;
}

function makeBuilder(executeSpy: Spy, valuesSpy: Spy, setSpy: Spy): QueryBuilder {
  const builder: QueryBuilder = {} as QueryBuilder;
  builder.where = () => builder;
  builder.selectAll = () => builder;
  builder.select = () => builder;
  builder.innerJoin = () => builder;
  builder.values = (...args: unknown[]) => {
    valuesSpy(...args);
    return builder;
  };
  builder.returningAll = () => builder;
  builder.set = (...args: unknown[]) => {
    setSpy(...args);
    return builder;
  };
  builder.orderBy = () => builder;
  builder.limit = () => builder;
  builder.offset = () => builder;
  builder.forUpdate = () => builder;
  builder.executeTakeFirst = () => executeSpy();
  builder.executeTakeFirstOrThrow = () => executeSpy();
  builder.execute = () => executeSpy();
  return builder;
}

export function createMockDb<T = Transaction<Record<string, unknown>>>(): {
  db: T;
  spies: MockDbSpies;
  reset: () => void;
} {
  const select = vi.fn();
  const update = vi.fn();
  const insert = vi.fn();
  const del = vi.fn();
  const values = vi.fn();
  const set = vi.fn();

  const db = {
    isTransaction: true as const,
    selectFrom: () => makeBuilder(select, values, set),
    updateTable: () => makeBuilder(update, values, set),
    insertInto: () => makeBuilder(insert, values, set),
    deleteFrom: () => makeBuilder(del, values, set),
  } as unknown as T;

  return {
    db,
    spies: { select, update, insert, delete: del, values, set },
    reset: () => {
      select.mockReset();
      update.mockReset();
      insert.mockReset();
      del.mockReset();
      values.mockReset();
      set.mockReset();
    },
  };
}

export function testNotFound<TInput, TReturn extends Result<unknown, Error>>(
  command: Command<TInput, TReturn>,
  input: TInput,
  expectedError: abstract new (...args: never[]) => ErrorOf<TReturn>,
) {
  return async () => {
    const { db, spies } = createMockDb();
    spies.select.mockReturnValue(undefined);
    const ctx: CommandContext = { actorId: "test-actor", permissions: [] };
    const result = await command(db, input, ctx);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(expectedError);
    }
  };
}

export function testPermissionDenied<TInput, TReturn extends Result<unknown, Error>>(
  command: Command<TInput, TReturn>,
  input: TInput,
) {
  return async () => {
    const { db } = createMockDb();
    const denied: CommandContext = { actorId: "test-actor", permissions: [] };
    const result = await command(db, input, denied);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InsufficientPermissionError);
    }
  };
}

export function testIdempotent<
  TInput,
  TReturn extends Result<Record<string, unknown>, Error>,
  TKey extends string & keyof ValueOf<TReturn>,
>(
  command: Command<TInput, TReturn>,
  input: TInput,
  existingEntity: Record<string, unknown>,
  entityKey: TKey,
) {
  return async () => {
    const { db, spies } = createMockDb();
    spies.select.mockReturnValue(existingEntity);
    const ctx: CommandContext = { actorId: "test-actor", permissions: [] };
    const result = await command(db, input, ctx);
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value[entityKey]).toEqual(existingEntity);
    }
    expect(spies.update).not.toHaveBeenCalled();
  };
}
