// ---------------------------------------------------------------------------
// OpenAPI plugin — spec-in-blob storage coverage.
//
// The resolved spec text must live in the plugin blob store (content-addressed
// `spec/<sha256>`), NOT inline in `integration.config`: the catalog row rides
// along on every integrations list, so a multi-MB inline spec turns a
// metadata read into a bulk transfer. These tests pin:
//   - addSpec stores a pointer config (`specHash`, no inline `spec`) and the
//     blob round-trips through the store,
//   - the e2e path (addSpec → connection → invoke) works off the blob,
//   - legacy rows that still inline `spec` resolve tools unchanged,
//   - remove + re-add of the same spec is idempotent over the shared blob.
// ---------------------------------------------------------------------------

import { describe, expect, it } from "@effect/vitest";
import { Effect, Schema } from "effect";
import { FetchHttpClient, HttpServerRequest } from "effect/unstable/http";
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";

import {
  createExecutor,
  AuthTemplateSlug,
  ConnectionName,
  IntegrationSlug,
  ToolAddress,
  sha256Hex,
  type IntegrationConfig,
} from "@executor-js/sdk";
import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";
import { variable } from "@executor-js/sdk/http-auth";

import { openApiPlugin } from "./plugin";
import type { OpenapiStore } from "./store";
import type { AuthenticationInput } from "./types";
import {
  makeOpenApiHttpApiTestSourceConfig,
  serveOpenApiHttpApiTestServer,
  unwrapInvocation,
} from "../testing";

const testPlugins = (httpClientLayer = FetchHttpClient.layer) =>
  [openApiPlugin({ httpClientLayer }), memoryCredentialsPlugin()] as const;

const EchoHeaders = Schema.Struct({
  "x-api-key": Schema.optional(Schema.String),
});

const EchoGroup = HttpApiGroup.make("items").add(
  HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders }),
);
const TestApi = HttpApi.make("testApi").add(EchoGroup);

const EchoGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) =>
  handlers.handle("echoHeaders", () =>
    Effect.gen(function* () {
      const req = yield* HttpServerRequest.HttpServerRequest;
      return EchoHeaders.make({ "x-api-key": req.headers["x-api-key"] });
    }),
  ),
);

const specText = () => {
  const spec = makeOpenApiHttpApiTestSourceConfig(TestApi, {}).spec;
  if (spec.kind === "blob") return spec.value;
  return spec.url;
};

const apiKeyTemplate: AuthenticationInput = {
  slug: AuthTemplateSlug.make("apiKey"),
  type: "apiKey",
  headers: { "x-api-key": [variable("token")] },
};

describe("OpenAPI plugin — spec blob storage", () => {
  it.effect("addSpec stores a content pointer, not the inline spec text", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));
        const text = specText();

        yield* executor.openapi.addSpec({
          spec: { kind: "blob", value: text },
          slug: "blob_api",
        });

        const config = yield* executor.openapi.getConfig("blob_api");
        expect(Object.keys(config ?? {})).not.toContain("spec");
        expect(config?.specHash).toBe(yield* sha256Hex(text));
      }),
    ),
  );

  it.effect("invokes a tool end-to-end off the blob-backed spec", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const server = yield* serveOpenApiHttpApiTestServer({
          api: TestApi,
          handlersLayer: EchoGroupLive,
        });
        const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));

        yield* executor.openapi.addSpec({
          spec: { kind: "blob", value: server.specJson },
          slug: "blob_invoke",
          baseUrl: server.baseUrl,
          authenticationTemplate: [apiKeyTemplate],
        });
        yield* executor.connections.create({
          owner: "org",
          name: ConnectionName.make("main"),
          integration: IntegrationSlug.make("blob_invoke"),
          template: AuthTemplateSlug.make("apiKey"),
          value: "secret-key-123",
        });

        const result = unwrapInvocation(
          yield* executor.execute(
            ToolAddress.make("tools.blob_invoke.org.main.items.echoHeaders"),
            {},
          ),
        ).data as { "x-api-key"?: string };

        expect(result["x-api-key"]).toBe("secret-key-123");
      }),
    ),
  );

  it.effect("resolveTools reads the spec from the store, never an inline field", () =>
    Effect.gen(function* () {
      const plugin = openApiPlugin({ httpClientLayer: FetchHttpClient.layer });
      const text = specText();
      const hash = yield* sha256Hex(text);
      const storage: OpenapiStore = {
        putOperations: () => Effect.void,
        appendOperations: () => Effect.void,
        getOperation: () => Effect.succeed(null),
        listOperations: () => Effect.succeed([]),
        removeOperations: () => Effect.void,
        putSpec: () => Effect.void,
        getSpec: (specHash) => Effect.succeed(specHash === hash ? text : null),
        putDefs: () => Effect.void,
        getDefs: () => Effect.succeed(null),
      };

      const resolve = (config: IntegrationConfig) =>
        plugin.resolveTools!({
          integration: {
            slug: IntegrationSlug.make("pointer_api"),
            name: "pointer",
            description: "pointer",
            kind: "openapi",
            canRemove: true,
            canRefresh: false,
            authMethods: [],
          },
          config,
          connection: {
            owner: "org",
            integration: IntegrationSlug.make("pointer_api"),
            name: ConnectionName.make("main"),
          },
          template: null,
          storage,
          httpClientLayer: FetchHttpClient.layer,
          getValue: () => Effect.succeed(null),
          getValues: () => Effect.succeed({}),
        });

      const fromPointer = yield* resolve({ specHash: hash } as IntegrationConfig);
      expect(fromPointer.tools.map((tool) => String(tool.name))).toContain("items.echoHeaders");

      // A pre-migration row that still inlines `spec` yields no tools: the
      // spec-to-blob migrations rewrite those rows before this code runs, so
      // the runtime carries no inline-read path.
      const fromInline = yield* resolve({ spec: text } as IntegrationConfig);
      expect(fromInline.tools).toHaveLength(0);
    }),
  );

  it.effect("remove + re-add of the same spec is idempotent over the shared blob", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));
        const text = specText();

        yield* executor.openapi.addSpec({ spec: { kind: "blob", value: text }, slug: "re_add" });
        yield* executor.openapi.removeSpec("re_add");
        // The blob deliberately survives removal (another integration may share
        // the hash); re-adding must re-point at it without conflict.
        const second = yield* executor.openapi.addSpec({
          spec: { kind: "blob", value: text },
          slug: "re_add",
        });

        expect(second.toolCount).toBeGreaterThan(0);
        const config = yield* executor.openapi.getConfig("re_add");
        expect(config?.specHash).toBe(yield* sha256Hex(text));
      }),
    ),
  );
});
