// In-memory test double for `WorkOSVaultClient`.
//
// Mirrors the Effect-shaped surface of the real client (see ./client.ts) but
// stores objects in a `Map<string, WorkOSVaultObject>` keyed by name so tests
// never hit WorkOS. Errors carry a numeric `status` on `cause` so the
// production `isStatusError` checks in `secret-store.ts` match the same
// 404/409/400 paths the real SDK exercises.

import { Data, Effect } from "effect";

import {
  WorkOSVaultClientError,
  type WorkOSVaultClient,
  type WorkOSVaultObject,
  type WorkOSVaultObjectMetadata,
  type WorkOSVaultPromiseApi,
} from "./client";

export class TestWorkOSVaultNotFoundError extends Data.TaggedError("TestWorkOSVaultNotFoundError")<{
  readonly message: string;
  readonly status: 404;
}> {}

export class TestWorkOSVaultConflictError extends Data.TaggedError("TestWorkOSVaultConflictError")<{
  readonly message: string;
  readonly status: 409;
}> {}

export class TestWorkOSVaultInvalidRequestError extends Data.TaggedError(
  "TestWorkOSVaultInvalidRequestError",
)<{
  readonly message: string;
  readonly status: 400;
}> {}

type TestWorkOSVaultError =
  | TestWorkOSVaultNotFoundError
  | TestWorkOSVaultConflictError
  | TestWorkOSVaultInvalidRequestError;

export interface TestWorkOSVaultClientOptions {
  /**
   * Injects a single 409 on the next update against an object whose name
   * ends in `/secrets/conflict`. The retry path in the secret store should
   * then re-read and succeed on the second attempt.
   */
  readonly conflictOnNextSecretUpdate?: boolean;
  /**
   * Reject create/read with a 400 when the name contains a colon. Useful
   * for exercising the secret store's invalid-name fallback paths.
   */
  readonly rejectNamesWithColon?: boolean;
  /**
   * Reject reads with a 400 when the requested name is longer than this
   * threshold. Mirrors WorkOS's own length cap on object names.
   */
  readonly rejectReadNamesLongerThan?: number;
}

const notFound = (message: string) => new TestWorkOSVaultNotFoundError({ message, status: 404 });

const conflict = (message: string) => new TestWorkOSVaultConflictError({ message, status: 409 });

const invalidRequest = (message: string) =>
  new TestWorkOSVaultInvalidRequestError({ message, status: 400 });

const makeMetadata = (
  id: string,
  context: Record<string, string>,
  versionId: string,
): WorkOSVaultObjectMetadata => ({
  id,
  context,
  updatedAt: new Date(),
  versionId,
});

export const makeTestWorkOSVaultClient = (
  options?: TestWorkOSVaultClientOptions,
): WorkOSVaultClient => {
  const objects = new Map<string, WorkOSVaultObject>();
  let sequence = 0;
  let conflictPending = options?.conflictOnNextSecretUpdate ?? false;

  const nextId = () => `vault_${(sequence += 1)}_${crypto.randomUUID().slice(0, 8)}`;

  const validateObjectName = (name: string): Effect.Effect<void, TestWorkOSVaultError> => {
    if (options?.rejectNamesWithColon && name.includes(":")) {
      return Effect.fail(invalidRequest(`Invalid object name "${name}"`));
    }
    return Effect.void;
  };

  const validateReadName = (name: string): Effect.Effect<void, TestWorkOSVaultError> =>
    Effect.gen(function* () {
      yield* validateObjectName(name);
      if (
        options?.rejectReadNamesLongerThan !== undefined &&
        name.length > options.rejectReadNamesLongerThan
      ) {
        return yield* invalidRequest(`Invalid object name "${name}"`);
      }
    });

  const createObject = (opts: {
    readonly name: string;
    readonly value: string;
    readonly context: Record<string, string>;
  }): Effect.Effect<WorkOSVaultObjectMetadata, TestWorkOSVaultError> =>
    Effect.gen(function* () {
      yield* validateObjectName(opts.name);
      if (objects.has(opts.name)) {
        return yield* conflict(`Object "${opts.name}" already exists`);
      }
      const id = nextId();
      const metadata = makeMetadata(id, opts.context, `${id}-v1`);
      objects.set(opts.name, {
        id,
        name: opts.name,
        value: opts.value,
        metadata,
      });
      return metadata;
    });

  const readObjectByName = (name: string): Effect.Effect<WorkOSVaultObject, TestWorkOSVaultError> =>
    Effect.gen(function* () {
      yield* validateReadName(name);
      const object = objects.get(name);
      if (!object) {
        return yield* notFound(`Object "${name}" not found`);
      }
      return object;
    });

  const updateObject = (opts: {
    readonly id: string;
    readonly value: string;
    readonly versionCheck?: string;
  }): Effect.Effect<WorkOSVaultObject, TestWorkOSVaultError> =>
    Effect.gen(function* () {
      const current = [...objects.values()].find((o) => o.id === opts.id);
      if (!current) {
        return yield* notFound(`Object "${opts.id}" not found`);
      }
      if (conflictPending && current.name.endsWith("/secrets/conflict")) {
        conflictPending = false;
        return yield* conflict(`Injected conflict for "${opts.id}"`);
      }
      if (opts.versionCheck && current.metadata.versionId !== opts.versionCheck) {
        return yield* conflict(`Version mismatch for "${opts.id}"`);
      }

      const nextVersion = current.metadata.versionId.replace(
        /v(\d+)$/,
        (_, version) => `v${Number(version) + 1}`,
      );
      const next: WorkOSVaultObject = {
        ...current,
        value: opts.value,
        metadata: {
          ...current.metadata,
          updatedAt: new Date(),
          versionId: nextVersion,
        },
      };
      objects.set(current.name, next);
      return next;
    });

  const deleteObject = (opts: { readonly id: string }): Effect.Effect<void, TestWorkOSVaultError> =>
    Effect.gen(function* () {
      const entry = [...objects.entries()].find(([, object]) => object.id === opts.id);
      if (!entry) {
        return yield* notFound(`Object "${opts.id}" not found`);
      }
      objects.delete(entry[0]);
    });

  const wrap = <A>(
    operation: string,
    effect: Effect.Effect<A, TestWorkOSVaultError>,
  ): Effect.Effect<A, WorkOSVaultClientError> =>
    effect.pipe(
      Effect.mapError((cause) => new WorkOSVaultClientError({ cause, operation })),
      Effect.withSpan(`workos_vault.test.${operation}`),
    );

  // Promise-shaped facade exposed to `use` callers, which may be plugin code
  // that still calls into the underlying SDK directly via `client.use(...)`.
  // Each method runs the in-memory effect and rethrows the tagged error so
  // callers see the same `.status` shape they would from a real SDK rejection.
  const rawClient: WorkOSVaultPromiseApi = {
    createObject: (opts) => Effect.runPromise(createObject(opts)),
    readObjectByName: (name) => Effect.runPromise(readObjectByName(name)),
    updateObject: (opts) => Effect.runPromise(updateObject(opts)),
    deleteObject: (opts) => Effect.runPromise(deleteObject(opts)),
  };

  return {
    use: (operation, fn) =>
      Effect.tryPromise({
        try: () => fn(rawClient),
        catch: (cause) => new WorkOSVaultClientError({ cause, operation }),
      }).pipe(Effect.withSpan(`workos_vault.test.${operation}`)),
    createObject: (opts) => wrap("create_object", createObject(opts)),
    readObjectByName: (name) => wrap("read_object_by_name", readObjectByName(name)),
    updateObject: (opts) => wrap("update_object", updateObject(opts)),
    deleteObject: (opts) => wrap("delete_object", deleteObject(opts)),
  };
};
