// Refresh endpoint — covers `sources.refresh(id)` for an OpenAPI
// source added from a URL. Stands up a local HTTP server that serves
// one of two spec versions (swappable mid-test) so we can verify the
// refresh path re-fetches from the stored origin and replaces the
// operation set. Raw-text sources assert the no-op branch.

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

import { ScopeId } from "@executor-js/sdk";
import {
  makeOpenApiHttpApiTestSpecPayload,
  serveMutableOpenApiSpecTestServer,
} from "@executor-js/plugin-openapi/testing";

import { asOrg } from "./__test-harness__/api-harness";

const PingEndpoint = HttpApiEndpoint.get("ping", "/ping", { success: Schema.Unknown });
const PongEndpoint = HttpApiEndpoint.get("pong", "/pong", { success: Schema.Unknown });

const RefreshGroupV1 = HttpApiGroup.make("default", { topLevel: true }).add(PingEndpoint);
const RefreshGroupV2 = HttpApiGroup.make("default", { topLevel: true })
  .add(PingEndpoint)
  .add(PongEndpoint);

const refreshApi = (version: "1.0.0" | "2.0.0") =>
  HttpApi.make("refreshFixture")
    .add(version === "1.0.0" ? RefreshGroupV1 : RefreshGroupV2)
    .annotateMerge(OpenApi.annotations({ title: "Refresh Fixture", version }));

const makeRefreshSpecText = () => makeOpenApiHttpApiTestSpecPayload(refreshApi("1.0.0")).spec;

describe("sources.refresh (HTTP)", () => {
  it.effect("addSpec from URL → canRefresh:true; refresh re-fetches and updates tools", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const server = yield* serveMutableOpenApiSpecTestServer({
          initialApi: refreshApi("1.0.0"),
        });
        const org = `org_${crypto.randomUUID()}`;
        const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`;

        yield* asOrg(org, (client) =>
          client.openapi.addSpec({
            params: { scopeId: ScopeId.make(org) },
            payload: {
              spec: { kind: "url", url: server.specUrl },
              name: namespace,
              baseUrl: server.baseUrl,
              namespace,
            },
          }),
        );

        const before = yield* asOrg(org, (client) =>
          client.sources.list({ params: { scopeId: ScopeId.make(org) } }),
        );
        const beforeSource = before.find((s) => s.id === namespace);
        expect(beforeSource?.canRefresh).toBe(true);

        const fetchedBefore = yield* asOrg(org, (client) =>
          client.openapi.getSource({
            params: { scopeId: ScopeId.make(org), namespace },
          }),
        );
        expect(fetchedBefore?.config.sourceUrl).toBe(server.specUrl);

        const beforeTools = yield* asOrg(org, (client) =>
          client.sources.tools({
            params: { scopeId: ScopeId.make(org), sourceId: namespace },
          }),
        );
        expect(beforeTools.length).toBe(1);
        expect(beforeTools.some((t) => t.id.endsWith(".default.ping"))).toBe(true);
        expect(beforeTools.some((t) => t.id.endsWith(".default.pong"))).toBe(false);

        // Flip the remote to v2 (adds `pong`) and trigger refresh.
        yield* server.setApi(refreshApi("2.0.0"));
        const requestsBefore = yield* server.requestCount;

        const refreshResult = yield* asOrg(org, (client) =>
          client.sources.refresh({
            params: { scopeId: ScopeId.make(org), sourceId: namespace },
          }),
        );
        expect(refreshResult.refreshed).toBe(true);
        expect(yield* server.requestCount).toBeGreaterThan(requestsBefore);

        const afterTools = yield* asOrg(org, (client) =>
          client.sources.tools({
            params: { scopeId: ScopeId.make(org), sourceId: namespace },
          }),
        );
        expect(afterTools.length).toBe(2);
        expect(afterTools.some((t) => t.id.endsWith(".default.ping"))).toBe(true);
        expect(afterTools.some((t) => t.id.endsWith(".default.pong"))).toBe(true);
      }),
    ),
  );

  it.effect("addSpec from raw text → canRefresh:false; refresh is a no-op", () =>
    Effect.gen(function* () {
      const org = `org_${crypto.randomUUID()}`;
      const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`;

      yield* asOrg(org, (client) =>
        client.openapi.addSpec({
          params: { scopeId: ScopeId.make(org) },
          payload: {
            spec: { kind: "blob", value: makeRefreshSpecText() },
            name: namespace,
            baseUrl: "https://api.example.test",
            namespace,
          },
        }),
      );

      const sources = yield* asOrg(org, (client) =>
        client.sources.list({ params: { scopeId: ScopeId.make(org) } }),
      );
      const row = sources.find((s) => s.id === namespace);
      expect(row?.canRefresh).toBe(false);

      // Raw-text sources reach the plugin with no stored URL and
      // silently no-op — UI gates the action on canRefresh, but the
      // server should not 500 if a caller slips through.
      const result = yield* asOrg(org, (client) =>
        client.sources.refresh({
          params: { scopeId: ScopeId.make(org), sourceId: namespace },
        }),
      );
      expect(result.refreshed).toBe(true);
    }),
  );
});
