import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Path from "effect/Path"
import * as PlatformError from "effect/PlatformError"
import {
  HttpEffect,
  HttpPlatform,
  HttpRouter,
  HttpServerError,
  HttpServerRequest,
  HttpServerResponse,
  HttpStaticServer
} from "effect/unstable/http"

const root = "/root"
const filePath = `${root}/file.txt`
const fileBody = "hello static"
const lastModified = "Wed, 01 Jan 2025 00:00:00 GMT"

const notFoundError = (path: string) =>
  PlatformError.systemError({
    _tag: "NotFound",
    module: "FileSystem",
    method: "stat",
    description: "No such file or directory",
    pathOrDescriptor: path
  })

const permissionDeniedError = (path: string) =>
  PlatformError.systemError({
    _tag: "PermissionDenied",
    module: "FileSystem",
    method: "stat",
    description: "Operation not permitted",
    pathOrDescriptor: path
  })

const fileInfo: FileSystem.File.Info = {
  type: "File",
  mtime: Option.some(new Date(lastModified)),
  atime: Option.none(),
  birthtime: Option.none(),
  dev: 0,
  ino: Option.none(),
  mode: 0,
  nlink: Option.none(),
  uid: Option.none(),
  gid: Option.none(),
  rdev: Option.none(),
  size: FileSystem.Size(fileBody.length),
  blksize: Option.none(),
  blocks: Option.none()
}

const makeHandler = async () => {
  const fileSystem = FileSystem.makeNoop({
    stat: (path) => path === filePath ? Effect.succeed(fileInfo) : Effect.fail(notFoundError(path))
  })

  const httpPlatform = HttpPlatform.HttpPlatform.of({
    fileResponse: (_path, options) =>
      Effect.succeed(HttpServerResponse.text(fileBody, {
        status: options?.status,
        headers: {
          ETag: "\"etag-value\"",
          "Last-Modified": lastModified
        }
      })),
    fileWebResponse: () => Effect.die("not implemented")
  })

  const app = await Effect.runPromise(
    HttpStaticServer.make({
      root,
      cacheControl: "public, max-age=60"
    }).pipe(
      Effect.provide(Path.layer),
      Effect.provideService(FileSystem.FileSystem, fileSystem),
      Effect.provideService(HttpPlatform.HttpPlatform, httpPlatform)
    )
  )

  return HttpEffect.toWebHandler(app)
}

const makeFailingApp = async (options: {
  readonly statError?: PlatformError.PlatformError
  readonly fileResponseError?: PlatformError.PlatformError
}) => {
  const fileSystem = FileSystem.makeNoop({
    stat: (path) => {
      if (options.statError !== undefined) {
        return Effect.fail(options.statError)
      }
      return path === filePath ? Effect.succeed(fileInfo) : Effect.fail(notFoundError(path))
    }
  })

  const httpPlatform = HttpPlatform.HttpPlatform.of({
    fileResponse: (_path, fileOptions) => {
      if (options.fileResponseError !== undefined) {
        return Effect.fail(options.fileResponseError)
      }
      return Effect.succeed(HttpServerResponse.text(fileBody, {
        status: fileOptions?.status,
        headers: {
          ETag: "\"etag-value\"",
          "Last-Modified": lastModified
        }
      }))
    },
    fileWebResponse: () => Effect.die("not implemented")
  })

  return Effect.runPromise(
    HttpStaticServer.make({ root }).pipe(
      Effect.provide(Path.layer),
      Effect.provideService(FileSystem.FileSystem, fileSystem),
      Effect.provideService(HttpPlatform.HttpPlatform, httpPlatform)
    )
  )
}

const makeLayerHandler = (options: {
  readonly statError?: PlatformError.PlatformError
  readonly fileResponseError?: PlatformError.PlatformError
}) => {
  const fileSystem = FileSystem.makeNoop({
    stat: (path) => {
      if (options.statError !== undefined) {
        return Effect.fail(options.statError)
      }
      return path === filePath ? Effect.succeed(fileInfo) : Effect.fail(notFoundError(path))
    }
  })
  const httpPlatform = HttpPlatform.HttpPlatform.of({
    fileResponse: () =>
      options.fileResponseError !== undefined
        ? Effect.fail(options.fileResponseError)
        : Effect.succeed(HttpServerResponse.text(fileBody)),
    fileWebResponse: () => Effect.die("not implemented")
  })

  const dependencies = Layer.mergeAll(
    Path.layer,
    Layer.succeed(FileSystem.FileSystem, fileSystem),
    Layer.succeed(HttpPlatform.HttpPlatform, httpPlatform)
  )

  return HttpRouter.toWebHandler(
    HttpStaticServer.layer({ root }).pipe(Layer.provideMerge(dependencies)),
    { disableLogger: true }
  )
}

describe("HttpStaticServer", () => {
  it("304 with If-None-Match exact, weak, and wildcard", async () => {
    const handler = await makeHandler()
    const warmupResponse = await handler(new Request("http://localhost/file.txt"))
    const etag = warmupResponse.headers.get("etag")
    assert.strictEqual(etag, "\"etag-value\"")
    if (etag === null) {
      throw new Error("expected ETag header")
    }

    const exact = await handler(new Request("http://localhost/file.txt", { headers: { "If-None-Match": etag } }))
    const weak = await handler(new Request("http://localhost/file.txt", { headers: { "If-None-Match": `W/${etag}` } }))
    const list = await handler(
      new Request("http://localhost/file.txt", { headers: { "If-None-Match": `"other", W/${etag}` } })
    )
    const any = await handler(new Request("http://localhost/file.txt", { headers: { "If-None-Match": "*" } }))

    assert.deepStrictEqual([exact.status, weak.status, list.status, any.status], [304, 304, 304, 304])
    assert.strictEqual(exact.headers.get("etag"), "\"etag-value\"")
    assert.strictEqual(exact.headers.get("cache-control"), "public, max-age=60")
    assert.strictEqual(exact.headers.get("last-modified"), lastModified)
    assert.strictEqual(exact.headers.get("content-type"), null)
    assert.strictEqual(exact.headers.get("content-length"), null)
  })

  it("304 with If-Modified-Since and no If-None-Match", async () => {
    const handler = await makeHandler()

    const notModified = await handler(
      new Request("http://localhost/file.txt", {
        headers: {
          "If-Modified-Since": lastModified
        }
      })
    )
    const modified = await handler(
      new Request("http://localhost/file.txt", {
        headers: {
          "If-Modified-Since": "Tue, 31 Dec 2024 23:59:59 GMT"
        }
      })
    )
    const invalidDate = await handler(
      new Request("http://localhost/file.txt", {
        headers: {
          "If-Modified-Since": "not-a-date"
        }
      })
    )

    assert.strictEqual(notModified.status, 304)
    assert.strictEqual(modified.status, 200)
    assert.strictEqual(invalidDate.status, 200)
  })

  it("If-None-Match takes precedence over If-Modified-Since", async () => {
    const handler = await makeHandler()

    const response = await handler(
      new Request("http://localhost/file.txt", {
        headers: {
          "If-None-Match": "\"different\"",
          "If-Modified-Since": "Thu, 02 Jan 2025 00:00:00 GMT"
        }
      })
    )

    assert.strictEqual(response.status, 200)
  })

  it("matched If-None-Match takes precedence over Range", async () => {
    const handler = await makeHandler()

    const response = await handler(
      new Request("http://localhost/file.txt", {
        headers: {
          "If-None-Match": "\"etag-value\"",
          Range: "bytes=1000-1001"
        }
      })
    )

    assert.strictEqual(response.status, 304)
    assert.strictEqual(response.headers.get("etag"), "\"etag-value\"")
    assert.strictEqual(response.headers.get("cache-control"), "public, max-age=60")
    assert.strictEqual(response.headers.get("last-modified"), lastModified)
    assert.strictEqual(response.headers.get("content-range"), null)
    assert.strictEqual(response.headers.get("accept-ranges"), null)
    assert.strictEqual(response.headers.get("content-type"), null)
    assert.strictEqual(response.headers.get("content-length"), null)
  })

  it("wraps missing routes as HttpServerError RouteNotFound", async () => {
    const app = await makeFailingApp({})
    const error = await Effect.runPromise(
      app.pipe(
        Effect.provideService(
          HttpServerRequest.HttpServerRequest,
          HttpServerRequest.fromWeb(new Request("http://localhost/missing.txt"))
        ),
        Effect.flip
      )
    )

    assert.instanceOf(error, HttpServerError.HttpServerError)
    assert.strictEqual(error.reason._tag, "RouteNotFound")
  })

  it("maps non-NotFound platform failures to HttpServerError InternalError", async () => {
    const statError = permissionDeniedError(filePath)
    const app = await makeFailingApp({ statError })
    const error = await Effect.runPromise(
      app.pipe(
        Effect.provideService(
          HttpServerRequest.HttpServerRequest,
          HttpServerRequest.fromWeb(new Request("http://localhost/file.txt"))
        ),
        Effect.flip
      )
    )

    assert.instanceOf(error, HttpServerError.HttpServerError)
    assert.strictEqual(error.reason._tag, "InternalError")
    assert.strictEqual(error.reason.cause, statError)
  })

  it("layer renders InternalError responses as 500", async () => {
    const { handler, dispose } = makeLayerHandler({
      fileResponseError: permissionDeniedError(filePath)
    })
    try {
      const response = await handler(new Request("http://localhost/file.txt"))
      assert.strictEqual(response.status, 500)
    } finally {
      await dispose()
    }
  })
})
