import { beforeEach, describe, expect, it, vi } from "vitest"

import type { ModuleDefinition } from "../shared/define-module"
import type { HttpClient } from "./base"
import type { RouteArgs, RouteDefinition } from "./proxy"
import { APIProxyHandler, createAPIProxy } from "./proxy"

describe("APIProxyHandler", () => {
  let mockClient: HttpClient
  let routes: Map<string, RouteDefinition>
  let handler: APIProxyHandler

  beforeEach(() => {
    mockClient = {
      request: vi.fn(),
    } as any

    routes = new Map([
      ["get", { method: "GET", path: "/test" }],
      ["create", { method: "POST", path: "/test" }],
      ["update", { method: "PUT", path: "/test/{id}", params: ["id"] }],
      ["nested.get", { method: "GET", path: "/test/nested" }],
      [
        "nested.create",
        { method: "POST", path: "/test/nested/{id}", params: ["id"] },
      ],
      [
        "deeply.nested.action",
        { method: "POST", path: "/test/deeply/nested/action" },
      ],
      // Routes with explicit parameter classification
      [
        "withClassification",
        {
          method: "POST",
          path: "/test/{id}",
          params: ["id"],
          query: ["includeMetadata", "format"],
          body: ["title", "content"],
        },
      ],
      [
        "getWithQuery",
        {
          method: "GET",
          path: "/test/search",
          query: ["keyword", "limit", "offset"],
        },
      ],
    ])

    handler = new APIProxyHandler(mockClient, routes)
  })

  describe("Route Function Creation", () => {
    it("should create a route function for basic GET request", async () => {
      const mockResponse = { data: "test" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "get")

      expect(typeof routeFunction).toBe("function")

      const result = await routeFunction()

      expect(mockClient.request).toHaveBeenCalledWith("/test", {
        method: "GET",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
      })
      expect(result).toBe(mockResponse)
    })

    it("should create a route function for POST request with body", async () => {
      const mockResponse = { data: "created" }
      const requestBody = { name: "test", value: 123 }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "create")

      const result = await routeFunction(requestBody)

      expect(mockClient.request).toHaveBeenCalledWith("/test", {
        method: "POST",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        body: requestBody,
      })
      expect(result).toBe(mockResponse)
    })

    it("should create a route function with parameters", async () => {
      const mockResponse = { data: "updated" }
      const requestBody = { name: "updated" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "update")

      const result = await routeFunction({
        id: "123",
        ...requestBody,
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test/123", {
        method: "PUT",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        body: requestBody,
      })
      expect(result).toBe(mockResponse)
    })

    it("should handle query parameters", async () => {
      const mockResponse = { data: "test" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "get")

      const result = await routeFunction({
        limit: 10,
        offset: 0,
        active: true,
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test", {
        method: "GET",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        query: { limit: 10, offset: 0, active: true },
      })
      expect(result).toBe(mockResponse)
    })

    it("should handle all route arguments", async () => {
      const mockResponse = { data: "test" }
      const controller = new AbortController()
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "update")

      const inputArgs = {
        id: "456", // path param
        include: "metadata", // will go to query for GET, body for PUT
        name: "test", // will go to body for PUT
      }
      const args: RouteArgs = {
        headers: { "X-Custom": "header" },
        timeout: 5000,
        signal: controller.signal,
      }

      const result = await routeFunction(inputArgs, args)

      expect(mockClient.request).toHaveBeenCalledWith("/test/456", {
        method: "PUT",
        headers: { "X-Custom": "header" },
        timeout: 5000,
        signal: controller.signal,
        body: { include: "metadata", name: "test" },
      })
      expect(result).toBe(mockResponse)
    })
  })

  describe("Nested Routes", () => {
    it("should handle nested routes", async () => {
      const mockResponse = { data: "nested" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const nestedProxy = handler.get(target, "nested")

      expect(typeof nestedProxy).toBe("object")
      expect(typeof nestedProxy.get).toBe("function")

      const result = await nestedProxy.get()

      expect(mockClient.request).toHaveBeenCalledWith("/test/nested", {
        method: "GET",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
      })
      expect(result).toBe(mockResponse)
    })

    it("should handle nested routes with parameters", async () => {
      const mockResponse = { data: "nested created" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const nestedProxy = handler.get(target, "nested")

      const result = await nestedProxy.create({
        id: "nested-123",
        data: "nested",
      })

      expect(mockClient.request).toHaveBeenCalledWith(
        "/test/nested/nested-123",
        {
          method: "POST",
          headers: undefined,
          timeout: undefined,
          signal: undefined,
          body: { data: "nested" },
        },
      )
      expect(result).toBe(mockResponse)
    })

    it("should handle deeply nested routes", async () => {
      const mockResponse = { data: "deeply nested" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const deeplyProxy = handler.get(target, "deeply")
      const nestedProxy = deeplyProxy.nested

      expect(typeof nestedProxy).toBe("object")
      expect(typeof nestedProxy.action).toBe("function")

      const result = await nestedProxy.action({ test: true })

      expect(mockClient.request).toHaveBeenCalledWith(
        "/test/deeply/nested/action",
        {
          method: "POST",
          headers: undefined,
          timeout: undefined,
          signal: undefined,
          body: { test: true },
        },
      )
      expect(result).toBe(mockResponse)
    })
  })

  describe("Error Handling", () => {
    it("should throw error for non-existent route", () => {
      const target = {}

      expect(() => {
        handler.get(target, "nonexistent")
      }).toThrow("Route 'nonexistent' not found")
    })

    it("should throw error for nested route that doesn't exist", () => {
      const target = {}
      const nestedProxy = handler.get(target, "nested")

      expect(() => {
        void nestedProxy.nonexistent
      }).toThrow("Route 'nonexistent' not found")
    })

    it("should handle client request errors", async () => {
      const error = new Error("Network error")
      mockClient.request = vi.fn().mockRejectedValue(error)

      const target = {}
      const routeFunction = handler.get(target, "get")

      await expect(routeFunction()).rejects.toThrow("Network error")
    })
  })

  describe("Special Properties", () => {
    it("should handle constructor property", () => {
      const target = { constructor: Function }
      const result = handler.get(target, "constructor")

      expect(result).toBe(Function)
    })

    it("should handle prototype property", () => {
      const target = { prototype: {} }
      const result = handler.get(target, "prototype")

      expect(result).toBe(target.prototype)
    })
  })

  describe("Parameter Classification", () => {
    it("should classify parameters according to route definition", async () => {
      const mockResponse = { data: "classified" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "withClassification")

      const result = await routeFunction({
        id: "123", // path param
        includeMetadata: true, // query param
        format: "json", // query param
        title: "Test Title", // body param
        content: "Test Content", // body param
        extra: "should be in body", // remaining field → body (POST)
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test/123", {
        method: "POST",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        query: { includeMetadata: true, format: "json" },
        body: {
          title: "Test Title",
          content: "Test Content",
          extra: "should be in body",
        },
      })
      expect(result).toBe(mockResponse)
    })

    it("should classify GET route parameters correctly", async () => {
      const mockResponse = { data: "search results" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "getWithQuery")

      const result = await routeFunction({
        keyword: "typescript", // query param
        limit: 10, // query param
        offset: 0, // query param
        extra: "should be in query", // remaining field → query (GET)
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test/search", {
        method: "GET",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        query: {
          keyword: "typescript",
          limit: 10,
          offset: 0,
          extra: "should be in query",
        },
      })
      expect(result).toBe(mockResponse)
    })

    it("should handle missing optional classified fields", async () => {
      const mockResponse = { data: "partial" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "withClassification")

      const result = await routeFunction({
        id: "456", // path param
        title: "Only Title", // body param
        // Missing: includeMetadata, format, content
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test/456", {
        method: "POST",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        body: { title: "Only Title" },
      })
      expect(result).toBe(mockResponse)
    })

    it("should work with flattened API style", async () => {
      const mockResponse = { data: "flattened" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "withClassification")

      // Simulate flattened input - user doesn't need to know about params/query/body
      const result = await routeFunction({
        id: "789",
        includeMetadata: false,
        title: "Flattened Title",
        content: "Flattened Content",
        format: "xml",
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test/789", {
        method: "POST",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        query: { includeMetadata: false, format: "xml" },
        body: { title: "Flattened Title", content: "Flattened Content" },
      })
      expect(result).toBe(mockResponse)
    })

    it("should handle empty body correctly", async () => {
      const mockResponse = { data: "no body" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "withClassification")

      const result = await routeFunction({
        id: "empty",
        includeMetadata: true,
        // No body fields provided
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test/empty", {
        method: "POST",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        query: { includeMetadata: true },
      })
      expect(result).toBe(mockResponse)
    })

    it("should preserve backward compatibility for unclassified routes", async () => {
      const mockResponse = { data: "backward compatible" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const getFunction = handler.get(target, "get")
      const createFunction = handler.get(target, "create")

      // GET route - all fields should go to query
      await getFunction({
        search: "test",
        limit: 10,
        active: true,
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test", {
        method: "GET",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        query: { search: "test", limit: 10, active: true },
      })

      // POST route - all fields should go to body
      await createFunction({
        name: "test",
        value: 123,
        active: false,
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test", {
        method: "POST",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
        body: { name: "test", value: 123, active: false },
      })
    })
  })

  describe("Parameter Encoding", () => {
    it("should encode URL parameters", async () => {
      const mockResponse = { data: "test" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "update")

      await routeFunction({
        id: "test/with/slashes",
      })

      expect(mockClient.request).toHaveBeenCalledWith(
        "/test/test%2Fwith%2Fslashes",
        {
          method: "PUT",
          headers: undefined,
          timeout: undefined,
          signal: undefined,
        },
      )
    })

    it("should handle undefined parameters", async () => {
      const mockResponse = { data: "test" }
      mockClient.request = vi.fn().mockResolvedValue(mockResponse)

      const target = {}
      const routeFunction = handler.get(target, "update")

      await routeFunction({
        id: undefined,
      })

      expect(mockClient.request).toHaveBeenCalledWith("/test/{id}", {
        method: "PUT",
        headers: undefined,
        timeout: undefined,
        signal: undefined,
      })
    })
  })
})

describe("createAPIProxy", () => {
  let mockClient: HttpClient
  let moduleDefinition: ModuleDefinition<any>

  beforeEach(() => {
    mockClient = {
      request: vi.fn(),
    } as any

    moduleDefinition = {
      api: {},
      name: "test",
      prefix: "/api/v1",
      routes: {
        get: { method: "GET", path: "/" },
        create: { method: "POST", path: "/" },
        update: { method: "PUT", path: "/{id}", params: ["id"] },
        nested: {
          get: { method: "GET", path: "/nested" },
          create: { method: "POST", path: "/nested/{id}", params: ["id"] },
        },
      },
    }

    // Mock the RouteResolver
    vi.mock("../shared/route-resolver", () => ({
      RouteResolver: {
        flattenRoutes: vi.fn().mockReturnValue({
          "get": { method: "GET", path: "/api/v1/" },
          "create": { method: "POST", path: "/api/v1/" },
          "update": { method: "PUT", path: "/api/v1/{id}", params: ["id"] },
          "nested.get": { method: "GET", path: "/api/v1/nested" },
          "nested.create": {
            method: "POST",
            path: "/api/v1/nested/{id}",
            params: ["id"],
          },
        }),
      },
    }))
  })

  it("should create a typed API proxy", async () => {
    const mockResponse = { data: "test" }
    mockClient.request = vi.fn().mockResolvedValue(mockResponse)

    const api = createAPIProxy(mockClient, moduleDefinition)

    expect(typeof api).toBe("object")
    expect(typeof api.get).toBe("function")
    expect(typeof api.create).toBe("function")
    expect(typeof api.update).toBe("function")
    expect(typeof api.nested).toBe("object")
    expect(typeof api.nested.get).toBe("function")
    expect(typeof api.nested.create).toBe("function")

    const result = await api.get()

    expect(mockClient.request).toHaveBeenCalledWith("/api/v1/", {
      method: "GET",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
    })
    expect(result).toBe(mockResponse)
  })

  it("should handle nested routes in created proxy", async () => {
    const mockResponse = { data: "nested" }
    mockClient.request = vi.fn().mockResolvedValue(mockResponse)

    const api = createAPIProxy(mockClient, moduleDefinition)

    const result = await api.nested.get()

    expect(mockClient.request).toHaveBeenCalledWith("/api/v1/nested", {
      method: "GET",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
    })
    expect(result).toBe(mockResponse)
  })

  it("should handle nested routes with parameters", async () => {
    const mockResponse = { data: "nested created" }
    mockClient.request = vi.fn().mockResolvedValue(mockResponse)

    const api = createAPIProxy(mockClient, moduleDefinition)

    const result = await api.nested.create({
      id: "123",
      name: "test",
    })

    expect(mockClient.request).toHaveBeenCalledWith("/api/v1/nested/123", {
      method: "POST",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
      body: { name: "test" },
    })
    expect(result).toBe(mockResponse)
  })

  it("should handle route with parameters", async () => {
    const mockResponse = { data: "updated" }
    mockClient.request = vi.fn().mockResolvedValue(mockResponse)

    const api = createAPIProxy(mockClient, moduleDefinition)

    const result = await api.update({
      id: "456",
      name: "updated",
    })

    expect(mockClient.request).toHaveBeenCalledWith("/api/v1/456", {
      method: "PUT",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
      body: { name: "updated" },
    })
    expect(result).toBe(mockResponse)
  })
})

describe("Proxy Integration", () => {
  let mockClient: HttpClient

  beforeEach(() => {
    mockClient = {
      request: vi.fn(),
    } as any
  })

  it("should work with complex nested structure", async () => {
    const routes = new Map<string, RouteDefinition>([
      ["admin.users.list", { method: "GET", path: "/admin/users" }],
      ["admin.users.create", { method: "POST", path: "/admin/users" }],
      [
        "admin.users.get",
        { method: "GET", path: "/admin/users/{id}", params: ["id"] },
      ],
      [
        "admin.users.update",
        { method: "PUT", path: "/admin/users/{id}", params: ["id"] },
      ],
      [
        "admin.users.delete",
        { method: "DELETE", path: "/admin/users/{id}", params: ["id"] },
      ],
      ["admin.settings.get", { method: "GET", path: "/admin/settings" }],
      ["admin.settings.update", { method: "PUT", path: "/admin/settings" }],
    ])

    const handler = new APIProxyHandler(mockClient, routes)
    const target = {}

    const mockResponse = { data: "success" }
    mockClient.request = vi.fn().mockResolvedValue(mockResponse)

    // Test nested admin.users.list
    const adminProxy = handler.get(target, "admin")
    const usersProxy = adminProxy.users

    await usersProxy.list()
    expect(mockClient.request).toHaveBeenCalledWith("/admin/users", {
      method: "GET",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
    })

    // Test nested admin.users.get with params
    await usersProxy.get({ id: "123" })
    expect(mockClient.request).toHaveBeenCalledWith("/admin/users/123", {
      method: "GET",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
    })

    // Test nested admin.settings.get
    const settingsProxy = adminProxy.settings
    await settingsProxy.get()
    expect(mockClient.request).toHaveBeenCalledWith("/admin/settings", {
      method: "GET",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
    })
  })

  it("should handle multiple levels of nesting", async () => {
    const routes = new Map<string, RouteDefinition>([
      ["a.b.c.d.action", { method: "POST", path: "/a/b/c/d/action" }],
    ])

    const handler = new APIProxyHandler(mockClient, routes)
    const target = {}

    const mockResponse = { data: "deep" }
    mockClient.request = vi.fn().mockResolvedValue(mockResponse)

    const result = await handler.get(target, "a").b.c.d.action()

    expect(mockClient.request).toHaveBeenCalledWith("/a/b/c/d/action", {
      method: "POST",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
    })
    expect(result).toBe(mockResponse)
  })

  it("should handle concurrent requests", async () => {
    const routes = new Map<string, RouteDefinition>([
      ["get", { method: "GET", path: "/test" }],
      ["post", { method: "POST", path: "/test" }],
    ])

    const handler = new APIProxyHandler(mockClient, routes)
    const target = {}

    const mockResponse = { data: "concurrent" }
    mockClient.request = vi.fn().mockResolvedValue(mockResponse)

    const getFunction = handler.get(target, "get")
    const postFunction = handler.get(target, "post")

    const promises = [
      getFunction(),
      postFunction({ data: "test" }),
      getFunction(),
    ]

    const results = await Promise.all(promises)

    expect(results).toHaveLength(3)
    expect(mockClient.request).toHaveBeenCalledTimes(3)
    expect(mockClient.request).toHaveBeenCalledWith("/test", {
      method: "GET",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
    })
    expect(mockClient.request).toHaveBeenCalledWith("/test", {
      method: "POST",
      headers: undefined,
      timeout: undefined,
      signal: undefined,
      body: { data: "test" },
    })
  })
})
