import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter"
import { assert, describe, it } from "@effect/vitest"
import { deepStrictEqual, strictEqual } from "@effect/vitest/utils"
import { Array, Context, Effect, Layer, Redacted, Ref, Schema } from "effect"
import { LanguageModel, Prompt } from "effect/unstable/ai"
import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http"

describe("OpenRouterLanguageModel", () => {
  describe("generateText", () => {
    describe("message preparation", () => {
      describe("audio file parts", () => {
        it.effect("converts audio bytes to input_audio", () =>
          Effect.gen(function*() {
            const audioData = new Uint8Array([0x49, 0x44, 0x33, 0x04]) // ID3v2 magic bytes

            yield* LanguageModel.generateText({
              prompt: Prompt.make([{
                role: "user",
                content: [
                  Prompt.filePart({
                    mediaType: "audio/mpeg",
                    data: audioData
                  })
                ]
              }])
            }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")))

            const requests = yield* MockHttpClient.requests
            const body = yield* getRequestBody(requests[0])

            const userMessage = body.messages.find((message: any) => message.role === "user")
            deepStrictEqual(userMessage.content, [{
              type: "input_audio",
              input_audio: {
                data: "SUQzBA==",
                format: "mp3"
              }
            }])
          }).pipe(Effect.provide(makeTestLayer())))

        it.effect("converts base64 data url audio to input_audio", () =>
          Effect.gen(function*() {
            yield* LanguageModel.generateText({
              prompt: Prompt.make([{
                role: "user",
                content: [
                  Prompt.filePart({
                    mediaType: "audio/wav",
                    data: "data:audio/wav;base64,UklGRg=="
                  })
                ]
              }])
            }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")))

            const requests = yield* MockHttpClient.requests
            const body = yield* getRequestBody(requests[0])

            const userMessage = body.messages.find((message: any) => message.role === "user")
            deepStrictEqual(userMessage.content, [{
              type: "input_audio",
              input_audio: {
                data: "UklGRg==",
                format: "wav"
              }
            }])
          }).pipe(Effect.provide(makeTestLayer())))

        it.effect("maps audio media types to OpenRouter audio formats", () =>
          Effect.gen(function*() {
            const mediaTypes: ReadonlyArray<readonly [mediaType: string, format: string]> = [
              ["audio/aac", "aac"],
              ["audio/x-aiff", "aiff"],
              ["audio/flac", "flac"],
              ["audio/L16", "pcm16"],
              ["audio/mp4", "m4a"],
              ["audio/mp3", "mp3"],
              ["audio/ogg", "ogg"],
              ["audio/x-wav", "wav"]
            ]

            yield* LanguageModel.generateText({
              prompt: Prompt.make([{
                role: "user",
                content: mediaTypes.map(([mediaType]) =>
                  Prompt.filePart({
                    mediaType,
                    data: new Uint8Array([0x00])
                  })
                )
              }])
            }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")))

            const requests = yield* MockHttpClient.requests
            const body = yield* getRequestBody(requests[0])

            const userMessage = body.messages.find((message: any) => message.role === "user")
            deepStrictEqual(
              userMessage.content.map((item: any) => item.input_audio.format),
              mediaTypes.map(([, format]) => format)
            )
          }).pipe(Effect.provide(makeTestLayer())))

        it.effect("fails on unsupported audio media types", () =>
          Effect.gen(function*() {
            const error = yield* LanguageModel.generateText({
              prompt: Prompt.make([{
                role: "user",
                content: [
                  Prompt.filePart({
                    mediaType: "audio/webm",
                    data: new Uint8Array([0x00])
                  })
                ]
              }])
            }).pipe(
              Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")),
              Effect.flip
            )

            strictEqual(error.reason._tag, "InvalidUserInputError")
            assert.include(error.message, "audio/webm")
          }).pipe(Effect.provide(makeTestLayer())))

        it.effect("fails on audio URLs", () =>
          Effect.gen(function*() {
            const error = yield* LanguageModel.generateText({
              prompt: Prompt.make([{
                role: "user",
                content: [
                  Prompt.filePart({
                    mediaType: "audio/mpeg",
                    data: new URL("https://example.com/audio.mp3")
                  })
                ]
              }])
            }).pipe(
              Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")),
              Effect.flip
            )

            strictEqual(error.reason._tag, "InvalidUserInputError")
          }).pipe(Effect.provide(makeTestLayer())))

        it.effect("converts non-audio files to file blocks", () =>
          Effect.gen(function*() {
            const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46]) // %PDF

            yield* LanguageModel.generateText({
              prompt: Prompt.make([{
                role: "user",
                content: [
                  Prompt.filePart({
                    mediaType: "application/pdf",
                    fileName: "document.pdf",
                    data: pdfData
                  })
                ]
              }])
            }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")))

            const requests = yield* MockHttpClient.requests
            const body = yield* getRequestBody(requests[0])

            const userMessage = body.messages.find((message: any) => message.role === "user")
            deepStrictEqual(userMessage.content, [{
              type: "file",
              file: {
                filename: "document.pdf",
                file_data: "data:application/pdf;base64,JVBERg=="
              }
            }])
          }).pipe(Effect.provide(makeTestLayer())))
      })
    })
  })
})

// =============================================================================
// Test Infrastructure
// =============================================================================

class MockOpenRouterResponse extends Context.Service<MockOpenRouterResponse, {
  readonly status: number
  readonly body: typeof Generated.SendChatCompletionRequest200.Type
  readonly headers?: Record<string, string> | undefined
}>()("MockOpenRouterResponse") {}

class MockHttpClient extends Context.Service<MockHttpClient, {
  readonly requests: Effect.Effect<ReadonlyArray<HttpClientRequest.HttpClientRequest>>
}>()("MockHttpClient") {
  static requests = Effect.service(MockHttpClient).pipe(
    Effect.flatMap((client) => client.requests)
  )
}

const encodeResponse = Schema.encodeEffect(Generated.SendChatCompletionRequest200)

const makeHttpClient = Effect.gen(function*() {
  const capturedRequests = yield* Ref.make<ReadonlyArray<HttpClientRequest.HttpClientRequest>>([])
  const response = yield* MockOpenRouterResponse
  const body = yield* Effect.orDie(encodeResponse(response.body))

  const httpClient = HttpClient.makeWith(
    Effect.fnUntraced(function*(requestEffect) {
      const request = yield* requestEffect
      yield* Ref.update(capturedRequests, Array.append(request))
      return HttpClientResponse.fromWeb(
        request,
        new Response(JSON.stringify(body), {
          headers: response.headers ?? {},
          status: response.status
        })
      )
    }),
    Effect.succeed as HttpClient.HttpClient.Preprocess<HttpClientError.HttpClientError, never>
  )

  return Context.make(HttpClient.HttpClient, httpClient).pipe(
    Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) }))
  )
})

const HttpClientLayer = Layer.effectContext(makeHttpClient)

const makeDefaultResponse = (
  overrides: Partial<typeof Generated.SendChatCompletionRequest200.Type> = {}
): typeof Generated.SendChatCompletionRequest200.Type => ({
  id: "gen-test123",
  choices: [{
    finish_reason: "stop",
    index: 0,
    message: {
      role: "assistant",
      content: "Hello!"
    }
  }],
  created: 1234567890,
  model: "google/gemini-2.5-flash",
  object: "chat.completion",
  ...overrides
})

const makeTestLayer = (options: {
  readonly body?: Partial<typeof Generated.SendChatCompletionRequest200.Type>
  readonly status?: number
  readonly headers?: Record<string, string>
} = {}) =>
  OpenRouterClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
    Layer.provideMerge(HttpClientLayer),
    Layer.provide(Layer.succeed(MockOpenRouterResponse, {
      body: makeDefaultResponse(options.body),
      status: options.status ?? 200,
      headers: options.headers ?? {}
    }))
  )

const getRequestBody = (request: HttpClientRequest.HttpClientRequest) =>
  Effect.gen(function*() {
    const body = request.body
    if (body._tag === "Uint8Array") {
      const text = new TextDecoder().decode(body.body)
      return JSON.parse(text)
    }
    return yield* Effect.die(new Error("Expected Uint8Array body"))
  })
