/* eslint-disable @typescript-eslint/no-empty-object-type */
import { describe, expect, it } from "vitest"

import type { InferParams, ModuleAPI, RouteFunction } from "./define-module"
import { defineModule, defineRoute } from "./define-module"

describe("define-module", () => {
  describe("defineRoute", () => {
    it("should define a simple GET route", () => {
      const route = defineRoute("GET", "/users")

      expect(route).toEqual({
        method: "GET",
        path: "/users",
        params: undefined,
        query: undefined,
        body: undefined,
        input: undefined,
        response: undefined,
        requestType: undefined,
        responseType: undefined,
      })
    })

    it("should extract params from path", () => {
      const route = defineRoute("GET", "/users/{userId}/posts/{postId}")

      expect(route.params).toEqual(["userId", "postId"])
    })

    it("should define route with query and body options", () => {
      const route = defineRoute("POST", "/users/{userId}", {
        query: ["include", "fields"] as const,
        body: ["name", "email"] as const,
        requestType: "json",
      })

      expect(route).toEqual({
        asRaw: undefined,
        method: "POST",
        path: "/users/{userId}",
        params: ["userId"],
        query: ["include", "fields"],
        body: ["name", "email"],
        input: undefined,
        response: undefined,
        requestType: "json",
      })
    })

    it("should handle path with no params", () => {
      const route = defineRoute("GET", "/health")

      expect(route.params).toBeUndefined()
    })

    it("should handle complex path patterns", () => {
      const route = defineRoute(
        "GET",
        "/api/v1/users/{userId}/posts/{postId}/comments",
      )

      expect(route.params).toEqual(["userId", "postId"])
    })
  })

  describe("defineModule", () => {
    it("should define a simple module", () => {
      const routes = {
        getUsers: defineRoute("GET", "/users"),
        createUser: defineRoute("POST", "/users"),
      }

      const module = defineModule({
        name: "users",
        prefix: "/api/v1",
        routes,
      })

      expect(module.name).toBe("users")
      expect(module.prefix).toBe("/api/v1")
      expect(module.routes).toBe(routes)
      expect(module.api).toEqual({})
    })

    it("should define module with nested routes", () => {
      const routes = {
        users: {
          list: defineRoute("GET", "/users"),
          create: defineRoute("POST", "/users"),
          byId: {
            get: defineRoute("GET", "/users/{userId}"),
            update: defineRoute("PUT", "/users/{userId}"),
            posts: {
              list: defineRoute("GET", "/users/{userId}/posts"),
              create: defineRoute("POST", "/users/{userId}/posts"),
            },
          },
        },
      }

      const module = defineModule({
        name: "api",
        routes,
      })

      expect(module.routes).toBe(routes)
    })
  })

  describe("Type inference", () => {
    it("should infer params from path string", () => {
      type UserPath = "/users/{userId}/posts/{postId}"
      type Params = InferParams<UserPath>

      const params: Params = {
        userId: "123",
        postId: "456",
      }

      expect(params.userId).toBe("123")
      expect(params.postId).toBe("456")
    })

    it("should handle path with no params", () => {
      type SimplePath = "/users"
      type Params = InferParams<SimplePath>

      const params: Params = {}
      expect(params).toEqual({})
    })
  })

  describe("RouteFunction types", () => {
    it("should create route function with required args", async () => {
      interface UserInput {
        name: string
        email: string
      }
      interface UserResponse {
        id: string
        name: string
        email: string
      }

      type CreateUserFn = RouteFunction<UserInput, UserResponse>

      // This should compile - required args
      const createUser: CreateUserFn = async (args, options) => {
        expect(args.name).toBeDefined()
        expect(args.email).toBeDefined()
        expect(options?.headers).toBeUndefined() // Optional second param

        return {
          id: "123",
          name: args.name,
          email: args.email,
        }
      }

      // Test the function call
      const result = createUser({ name: "John", email: "john@example.com" })
      await expect(result).resolves.toEqual({
        id: "123",
        name: "John",
        email: "john@example.com",
      })
    })

    it("should create route function with optional args", async () => {
      interface EmptyInput {}
      interface ListResponse {
        users: string[]
      }

      type ListUsersFn = RouteFunction<EmptyInput, ListResponse>

      // This should compile - optional args
      const listUsers: ListUsersFn = async (args, options) => {
        // When called without options, should be undefined
        // When called with options, should have the values
        if (options) {
          expect(options.timeout).toBe(5000)
        }

        return {
          users: ["user1", "user2"],
        }
      }

      // Both calls should work
      const result1 = listUsers()
      const result2 = listUsers({}, { timeout: 5000 })

      await expect(result1).resolves.toEqual({ users: ["user1", "user2"] })
      await expect(result2).resolves.toEqual({ users: ["user1", "user2"] })
    })

    it("should separate input args from fetch options", async () => {
      interface LoginInput {
        username: string
        password: string
      }
      interface LoginResponse {
        token: string
      }

      type LoginFn = RouteFunction<LoginInput, LoginResponse>

      const login: LoginFn = async (args, options) => {
        // First parameter: route input
        expect(args.username).toBe("testuser")
        expect(args.password).toBe("testpass")

        // Second parameter: fetch options
        expect(options?.headers?.["Authorization"]).toBe(
          "Bearer existing-token",
        )
        expect(options?.timeout).toBe(10000)
        expect(options?.signal).toBeInstanceOf(AbortSignal)

        return { token: "new-token" }
      }

      // Test with fetch options
      const controller = new AbortController()
      const result = login(
        { username: "testuser", password: "testpass" },
        {
          headers: { Authorization: "Bearer existing-token" },
          timeout: 10000,
          signal: controller.signal,
        },
      )

      await expect(result).resolves.toEqual({ token: "new-token" })
    })
  })

  describe("ModuleAPI types", () => {
    it("should generate correct API types from route definitions", () => {
      const _routes = {
        getUser: defineRoute("GET", "/users/{userId}", {
          input: {} as { userId: string },
          response: {} as { id: string, name: string },
        }),
        createUser: defineRoute("POST", "/users", {
          input: {} as { name: string, email: string },
          response: {} as { id: string, name: string, email: string },
        }),
        listUsers: defineRoute("GET", "/users", {
          input: {} as {},
          response: {} as { users: Array<{ id: string, name: string }> },
        }),
      }

      type API = ModuleAPI<typeof _routes>

      // This test ensures the types compile correctly
      const mockApi: API = {
        getUser: async (args) => {
          expect(args.userId).toBeDefined()
          return { id: args.userId, name: "Test User" }
        },
        createUser: async (args) => {
          expect(args.name).toBeDefined()
          expect(args.email).toBeDefined()
          return { id: "123", name: args.name, email: args.email }
        },
        listUsers: async () => {
          return { users: [{ id: "1", name: "User 1" }] }
        },
      }

      expect(mockApi.getUser).toBeDefined()
      expect(mockApi.createUser).toBeDefined()
      expect(mockApi.listUsers).toBeDefined()
    })

    it("should handle nested route structures", () => {
      const _routes = {
        users: {
          get: defineRoute("GET", "/users/{userId}", {
            input: {} as { userId: string },
            response: {} as { id: string, name: string },
          }),
          posts: {
            list: defineRoute("GET", "/users/{userId}/posts", {
              input: {} as { userId: string },
              response: {} as { posts: Array<{ id: string, title: string }> },
            }),
          },
        },
      }

      type API = ModuleAPI<typeof _routes>

      const mockApi: API = {
        users: {
          get: async (args) => {
            return { id: args.userId, name: "Test User" }
          },
          posts: {
            list: async () => {
              return { posts: [{ id: "1", title: "Post 1" }] }
            },
          },
        },
      }

      expect(mockApi.users.get).toBeDefined()
      expect(mockApi.users.posts.list).toBeDefined()
    })
  })

  describe("FetchOptions separation", () => {
    it("should handle fetch options independently from route input", async () => {
      interface TestInput {
        id: string
        data: { name: string }
      }
      interface TestResponse {
        success: boolean
      }

      type TestFn = RouteFunction<TestInput, TestResponse>

      const testFunction: TestFn = async (args, options) => {
        // Route input should not contain fetch options
        expect(args).toEqual({
          id: "123",
          data: { name: "test" },
        })

        // Fetch options should be separate
        expect(options).toEqual({
          headers: { "Content-Type": "application/json" },
          timeout: 5000,
        })

        return { success: true }
      }

      const result = testFunction(
        { id: "123", data: { name: "test" } },
        { headers: { "Content-Type": "application/json" }, timeout: 5000 },
      )

      await expect(result).resolves.toEqual({ success: true })
    })

    it("should allow omitting fetch options", async () => {
      interface TestInput {
        message: string
      }
      interface TestResponse {
        echo: string
      }

      type TestFn = RouteFunction<TestInput, TestResponse>

      const testFunction: TestFn = async (args, options) => {
        expect(args.message).toBe("hello")
        expect(options).toBeUndefined()

        return { echo: args.message }
      }

      const result = testFunction({ message: "hello" })
      await expect(result).resolves.toEqual({ echo: "hello" })
    })
  })

  describe("Legacy compatibility", () => {
    it("should maintain LegacyRouteArgs interface", () => {
      // This test ensures the legacy interface still exists
      const legacyArgs = {
        params: { userId: "123" },
        query: { include: "posts" },
        body: { name: "John" },
        headers: { Authorization: "Bearer token" },
        timeout: 5000,
        signal: new AbortController().signal,
      }

      expect(legacyArgs.params?.userId).toBe("123")
      expect(legacyArgs.query?.include).toBe("posts")
      expect(legacyArgs.body).toEqual({ name: "John" })
      expect(legacyArgs.headers?.Authorization).toBe("Bearer token")
      expect(legacyArgs.timeout).toBe(5000)
      expect(legacyArgs.signal).toBeInstanceOf(AbortSignal)
    })
  })
})
