import { expect, it } from "@effect/vitest";
import { Effect, Option } from "effect";

import {
  parseEntry,
  streamOperationBindingsFromStructure,
  structuralSplit,
} from "@executor-js/plugin-openapi";

import { microsoftGraphKeepPathItem } from "./graph";

// Mirrors the verbatim shape of the real Microsoft Graph v1.0 spec: every
// success response uses the OpenAPI wildcard status key `2XX` (the real spec
// has zero numeric 200/201 keys), drive content GET is already declared as a
// binary octet-stream, PUT declares a binary octet-stream requestBody, error
// responses are $refs, and path-level shared parameters carry
// `x-ms-docs-key-type`.
const driveContentFixture = `
openapi: 3.0.4
info:
  title: Microsoft Graph Fixture
  version: v1.0
servers:
  - url: https://graph.microsoft.com/v1.0
paths:
  /drives/{drive-id}/items/{driveItem-id}/content:
    get:
      tags:
        - drives.driveItem
      summary: Get content for the navigation property items from drives
      operationId: drives.GetItemsContent
      parameters:
        - name: $format
          in: query
          description: Format of the content
          style: form
          explode: false
          schema:
            type: string
      responses:
        2XX:
          description: Retrieved media content
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        4XX:
          $ref: '#/components/responses/error'
        5XX:
          $ref: '#/components/responses/error'
    put:
      tags:
        - drives.driveItem
      summary: Update content for the navigation property items in drives
      operationId: drives.UpdateItemsContent
      requestBody:
        description: New media content.
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
        required: true
      responses:
        2XX:
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/microsoft.graph.driveItem'
        4XX:
          $ref: '#/components/responses/error'
        5XX:
          $ref: '#/components/responses/error'
    delete:
      tags:
        - drives.driveItem
      summary: Delete content for the navigation property items in drives
      operationId: drives.DeleteItemsContent
      responses:
        '204':
          description: Success
        4XX:
          $ref: '#/components/responses/error'
        5XX:
          $ref: '#/components/responses/error'
    parameters:
      - name: drive-id
        in: path
        description: The unique identifier of drive
        required: true
        schema:
          type: string
        x-ms-docs-key-type: drive
      - name: driveItem-id
        in: path
        description: The unique identifier of driveItem
        required: true
        schema:
          type: string
        x-ms-docs-key-type: driveItem
components:
  schemas:
    microsoft.graph.driveItem:
      type: object
      properties:
        id:
          type: string
    microsoft.graph.ODataErrors.ODataError:
      type: object
      properties:
        error:
          type: object
  responses:
    error:
      description: error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/microsoft.graph.ODataErrors.ODataError'
`;

// Report-style Graph endpoints declare an octet-stream success response with an
// object schema (a `value` wrapper) instead of a binary string.
const reportContentFixture = `
openapi: 3.0.4
info:
  title: Microsoft Graph Fixture
  version: v1.0
servers:
  - url: https://graph.microsoft.com/v1.0
paths:
  /reports/getEmailActivityCounts(period={period}):
    get:
      tags:
        - reports.Functions
      summary: Invoke function getEmailActivityCounts
      operationId: reports.getEmailActivityCounts
      parameters:
        - name: period
          in: path
          required: true
          schema:
            type: string
      responses:
        2XX:
          description: Success
          content:
            application/octet-stream:
              schema:
                type: object
                properties:
                  value:
                    type: string
                    format: base64url
        4XX:
          $ref: '#/components/responses/error'
        5XX:
          $ref: '#/components/responses/error'
components:
  schemas:
    microsoft.graph.ODataErrors.ODataError:
      type: object
      properties:
        error:
          type: object
  responses:
    error:
      description: error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/microsoft.graph.ODataErrors.ODataError'
`;

const identityPathFixture = `
openapi: 3.0.4
info:
  title: Microsoft Graph Fixture
  version: v1.0
servers:
  - url: https://graph.microsoft.com/v1.0
paths:
  /me:
    get:
      operationId: me.GetUser
      security:
        - azureAdDelegated:
            - User.Read
      responses:
        "200":
          description: OK
    patch:
      operationId: me.UpdateUser
      security:
        - azureAdDelegated:
            - User.ReadWrite
      responses:
        "200":
          description: OK
  /me/photo:
    get:
      operationId: me.photo.GetProfilePhoto
      security:
        - azureAdDelegated:
            - User.Read
      responses:
        "200":
          description: OK
  /me/messages:
    get:
      operationId: me.messages.ListMessages
      security:
        - azureAdDelegated:
            - Mail.ReadWrite
      responses:
        "200":
          description: OK
components: {}
`;

const fullGraphSelection = {
  coversFullGraph: true,
  presetIds: [],
  customScopes: [],
  exactPaths: [],
  pathPrefixes: [],
  tagPrefixes: [],
} as const;

const noWorkloadSelection = {
  coversFullGraph: false,
  presetIds: [],
  customScopes: [],
  exactPaths: [],
  pathPrefixes: [],
  tagPrefixes: [],
} as const;

const profileSelection = {
  coversFullGraph: false,
  presetIds: ["profile"],
  customScopes: [],
  exactPaths: ["/me", "/me/photo", "/me/photo/$value"],
  pathPrefixes: [],
  tagPrefixes: [],
} as const;

type MicrosoftGraphKeepPathItemSelection = Parameters<typeof microsoftGraphKeepPathItem>[0];

const keptIdentityFixturePaths = (
  selection: MicrosoftGraphKeepPathItemSelection,
): Map<string, Record<string, unknown>> => {
  const structure = structuralSplit(identityPathFixture);
  expect(structure).not.toBeNull();
  const keepPathItem = microsoftGraphKeepPathItem(selection);
  const kept = new Map<string, Record<string, unknown>>();

  for (const range of structure!.pathItems) {
    const entry = parseEntry(structure!.text, range, 2);
    expect(entry).not.toBeNull();
    const [path, rawPathItem] = entry!;
    const pathItem = keepPathItem(path, rawPathItem as Record<string, unknown>);
    if (pathItem) kept.set(path, pathItem as Record<string, unknown>);
  }

  return kept;
};

const keptPathItem = (fixture: string): Record<string, unknown> => {
  const structure = structuralSplit(fixture);
  expect(structure).not.toBeNull();
  const entry = parseEntry(structure!.text, structure!.pathItems[0]!, 2);
  expect(entry).not.toBeNull();
  const [path, rawPathItem] = entry!;
  const pathItem = microsoftGraphKeepPathItem(fullGraphSelection)(
    path,
    rawPathItem as Record<string, unknown>,
  );
  expect(pathItem).not.toBeNull();
  return pathItem as Record<string, unknown>;
};

type StreamedBinding = {
  readonly binding: {
    readonly method: string;
    readonly pathTemplate: string;
    readonly responseBody: Option.Option<{
      readonly fileHint: Option.Option<{
        readonly kind: "binaryResponse" | "byteField";
      }>;
    }>;
  };
};

const streamBindings = (fixture: string) =>
  Effect.gen(function* () {
    const structure = structuralSplit(fixture);
    expect(structure).not.toBeNull();
    const chunks: StreamedBinding[] = [];
    yield* streamOperationBindingsFromStructure(
      structure!,
      { chunkSize: 10, keepPathItem: microsoftGraphKeepPathItem(fullGraphSelection) },
      (chunk) =>
        Effect.sync(() => {
          chunks.push(...chunk);
        }),
    );
    return chunks;
  });

const responseFileHintKind = (
  chunks: readonly StreamedBinding[],
  method: string,
  pathTemplate: string,
): string | undefined => {
  const match = chunks.find(
    (chunk) => chunk.binding.method === method && chunk.binding.pathTemplate === pathTemplate,
  );
  expect(match).toBeDefined();
  const hint = Option.flatMap(match!.binding.responseBody, (body) => body.fileHint);
  return Option.getOrUndefined(hint)?.kind;
};

it("keeps only GET /me for identity health checks without selected workloads", () => {
  const kept = keptIdentityFixturePaths(noWorkloadSelection);

  expect([...kept.keys()]).toEqual(["/me"]);
  expect(kept.get("/me")?.get).toMatchObject({ operationId: "me.GetUser" });
  expect(kept.get("/me")?.patch).toBeUndefined();
});

it("keeps all /me operations when profile selects the path", () => {
  const kept = keptIdentityFixturePaths(profileSelection);

  expect(kept.get("/me")?.get).toMatchObject({ operationId: "me.GetUser" });
  expect(kept.get("/me")?.patch).toMatchObject({ operationId: "me.UpdateUser" });
});

it("keeps already-binary drive content responses untouched", () => {
  const pathItem = keptPathItem(driveContentFixture);

  const get = pathItem.get as Record<string, unknown>;
  const getResponses = get.responses as Record<string, unknown>;
  expect(getResponses["2XX"]).toEqual({
    description: "Retrieved media content",
    content: {
      "application/octet-stream": {
        schema: { type: "string", format: "binary" },
      },
    },
  });
  expect(getResponses["4XX"]).toEqual({ $ref: "#/components/responses/error" });
  expect(getResponses["5XX"]).toEqual({ $ref: "#/components/responses/error" });

  // The real spec already declares the PUT requestBody as binary; the
  // normalization must not touch request bodies.
  const put = pathItem.put as Record<string, unknown>;
  expect(put.requestBody).toEqual({
    description: "New media content.",
    content: {
      "application/octet-stream": {
        schema: { type: "string", format: "binary" },
      },
    },
    required: true,
  });
  const putResponses = put.responses as Record<string, unknown>;
  expect(putResponses["2XX"]).toMatchObject({
    content: {
      "application/json": {
        schema: { $ref: "#/components/schemas/microsoft.graph.driveItem" },
      },
    },
  });

  // Path-level shared parameters survive the filter.
  expect(pathItem.parameters).toMatchObject([
    { name: "drive-id", "x-ms-docs-key-type": "drive" },
    { name: "driveItem-id", "x-ms-docs-key-type": "driveItem" },
  ]);
});

it("normalizes report-style octet-stream object schemas to binary strings", () => {
  const pathItem = keptPathItem(reportContentFixture);

  const get = pathItem.get as Record<string, unknown>;
  const responses = get.responses as Record<string, unknown>;
  const success = responses["2XX"] as Record<string, unknown>;
  expect(success.description).toBe("Success");
  expect(success.content).toEqual({
    "application/octet-stream": {
      schema: { type: "string", format: "binary" },
    },
  });
  expect(responses["4XX"]).toEqual({ $ref: "#/components/responses/error" });
});

it.effect("streams drive content download bindings with a binaryResponse file hint", () =>
  Effect.gen(function* () {
    const chunks = yield* streamBindings(driveContentFixture);
    expect(
      responseFileHintKind(chunks, "get", "/drives/{drive-id}/items/{driveItem-id}/content"),
    ).toBe("binaryResponse");
  }),
);

it.effect("streams report-style download bindings with a binaryResponse file hint", () =>
  Effect.gen(function* () {
    const chunks = yield* streamBindings(reportContentFixture);
    expect(
      responseFileHintKind(chunks, "get", "/reports/getEmailActivityCounts(period={period})"),
    ).toBe("binaryResponse");
  }),
);
