export interface FactoryContext {
  sequence: number;
}

export interface Factory<T> {
  build(overrides?: Partial<T>): T;
  buildList(count: number, overrides?: Partial<T>): T[];
  reset(): void;
}

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return typeof v === "object" && v !== null && Object.getPrototypeOf(v) === Object.prototype;
}

function deepMerge<T>(base: T, overrides: Partial<T>): T {
  if (!isPlainObject(base) || !isPlainObject(overrides)) {
    return (overrides ?? base) as T;
  }
  const result: Record<string, unknown> = { ...base };
  for (const key of Object.keys(overrides)) {
    const baseVal = (base as Record<string, unknown>)[key];
    const overrideVal = (overrides as Record<string, unknown>)[key];
    if (isPlainObject(baseVal) && isPlainObject(overrideVal)) {
      result[key] = deepMerge(baseVal, overrideVal);
    } else {
      result[key] = overrideVal;
    }
  }
  return result as T;
}

export function defineFactory<T extends object>(
  builder: (ctx: FactoryContext) => T,
): Factory<T> {
  let sequence = 0;
  return {
    build(overrides) {
      sequence += 1;
      const base = builder({ sequence });
      return deepMerge(base, overrides ?? {});
    },
    buildList(count, overrides) {
      return Array.from({ length: count }, () => this.build(overrides));
    },
    reset() {
      sequence = 0;
    },
  };
}
