import { describe, it, expect, vi, beforeEach } from "vitest"
import { renderHook, waitFor, act } from "@testing-library/react"
import React from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { useActorInfiniteQuery } from "./useActorInfiniteQuery"
import { ActorMethod } from "@icp-sdk/core/agent"
import { Reactor } from "@ic-reactor/core"

// Define a test actor type with paginated methods
type TestActor = {
  getPosts: ActorMethod<[{ page: number; limit: number }], string[]>
  getItems: ActorMethod<
    [number],
    { items: string[]; nextCursor: number | null }
  >
  getMessages: ActorMethod<
    [string],
    { messages: string[]; nextToken: string | null }
  >
}

// Mock data generators
const generatePosts = (page: number, limit: number): string[] => {
  const start = (page - 1) * limit
  return Array.from({ length: limit }, (_, i) => `Post ${start + i + 1}`)
}

const generateItems = (
  cursor: number
): { items: string[]; nextCursor: number | null } => {
  if (cursor >= 50) {
    return { items: [], nextCursor: null }
  }
  const items = Array.from({ length: 10 }, (_, i) => `Item ${cursor + i + 1}`)
  return { items, nextCursor: cursor + 10 }
}

// Mock Reactor
const createMockReactor = (queryClient: QueryClient) => {
  const callMethod = vi
    .fn()
    .mockImplementation(async ({ functionName, args }) => {
      if (functionName === "getPosts") {
        const { page, limit } = args[0]
        return generatePosts(page, limit)
      }
      if (functionName === "getItems") {
        const cursor = args[0]
        return generateItems(cursor)
      }
      if (functionName === "getMessages") {
        const token = args[0]
        const pageNum = token === "start" ? 0 : parseInt(token, 10)
        const messages = Array.from(
          { length: 5 },
          (_, i) => `Message ${pageNum * 5 + i + 1}`
        )
        const nextToken = pageNum < 3 ? String(pageNum + 1) : null
        return { messages, nextToken }
      }
      return null
    })

  return {
    queryClient,
    callMethod,
    generateQueryKey: vi
      .fn()
      .mockImplementation(({ functionName, args }) => [
        "test-canister",
        functionName,
        ...(args ? [JSON.stringify(args)] : []),
      ]),
  } as unknown as Reactor<TestActor>
}

describe("useActorInfiniteQuery", () => {
  let queryClient: QueryClient
  let mockReactor: ReturnType<typeof createMockReactor>

  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          retry: false,
          gcTime: 0,
        },
      },
    })
    mockReactor = createMockReactor(queryClient)
  })

  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  )

  describe("basic functionality", () => {
    it("should fetch initial page", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: (lastPage, allPages) => {
              if (lastPage.length < 10) return undefined
              return allPages.length + 1
            },
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      expect(result.current.data?.pages).toHaveLength(1)
      expect(result.current.data?.pages[0]).toHaveLength(10)
      expect(result.current.data?.pages[0][0]).toBe("Post 1")
    })

    it("should support fetchNextPage", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: (lastPage, allPages) => {
              if (lastPage.length < 10) return undefined
              return allPages.length + 1
            },
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      expect(result.current.hasNextPage).toBe(true)

      // fetchNextPage should be available and callable
      expect(typeof result.current.fetchNextPage).toBe("function")
      expect(result.current.isFetchingNextPage).toBe(false)
    })

    it("should call reactor.callMethod with correct args", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 5 }] as const,
            getNextPageParam: () => undefined,
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      expect(mockReactor.callMethod).toHaveBeenCalledWith({
        functionName: "getPosts",
        args: [{ page: 1, limit: 5 }],
        callConfig: undefined,
      })
    })
  })

  describe("cursor-based pagination", () => {
    it("should work with cursor-based pagination", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery<TestActor, "getItems", "candid", number>({
            reactor: mockReactor as unknown as Reactor<TestActor, "candid">,
            functionName: "getItems",
            initialPageParam: 0,
            getArgs: (cursor) => [cursor],
            getNextPageParam: (lastPage) => lastPage.nextCursor,
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      expect(result.current.data?.pages[0].items[0]).toBe("Item 1")
      expect(result.current.hasNextPage).toBe(true)

      await act(async () => {
        await result.current.fetchNextPage()
      })

      await waitFor(() => {
        expect(result.current.data?.pages).toHaveLength(2)
      })

      expect(result.current.data?.pages[1].items[0]).toBe("Item 11")
    })

    it("should determine hasNextPage based on getNextPageParam", async () => {
      // Create a mock that returns null nextCursor to indicate no more pages
      const noMorePagesMock = createMockReactor(queryClient)
      ;(
        noMorePagesMock.callMethod as ReturnType<typeof vi.fn>
      ).mockImplementation(async () => {
        return { items: ["Last Item"], nextCursor: null }
      })

      const { result } = renderHook(
        () =>
          useActorInfiniteQuery<TestActor, "getItems", "candid", number>({
            reactor: noMorePagesMock as unknown as Reactor<TestActor, "candid">,
            functionName: "getItems",
            initialPageParam: 0,
            getArgs: (cursor) => [cursor],
            getNextPageParam: (lastPage) => lastPage.nextCursor,
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      // When getNextPageParam returns null, hasNextPage should be false
      expect(result.current.hasNextPage).toBe(false)
    })
  })

  describe("token-based pagination", () => {
    it("should work with string token pagination", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery<TestActor, "getMessages", "candid", string>({
            reactor: mockReactor as unknown as Reactor<TestActor, "candid">,
            functionName: "getMessages",
            initialPageParam: "start",
            getArgs: (token) => [token],
            getNextPageParam: (lastPage) => lastPage.nextToken,
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      expect(result.current.data?.pages[0].messages[0]).toBe("Message 1")
    })
  })

  describe("query options", () => {
    it("should respect enabled: false option", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: () => undefined,
            enabled: false,
          }),
        { wrapper }
      )

      expect(result.current.isPending).toBe(true)
      expect(result.current.fetchStatus).toBe("idle")
      expect(mockReactor.callMethod).not.toHaveBeenCalled()
    })

    it("should use custom staleTime", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: () => undefined,
            staleTime: 1000 * 60 * 10, // 10 minutes
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      expect(result.current.isStale).toBe(false)
    })
  })

  describe("getPreviousPageParam", () => {
    it("should support bi-directional pagination", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 3,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: (lastPage, allPages) => {
              if (lastPage.length < 10) return undefined
              const lastPageParam = allPages.length + 2 // Started at page 3
              return lastPageParam + 1
            },
            getPreviousPageParam: (_firstPage, _allPages, firstPageParam) => {
              if (firstPageParam <= 1) return undefined
              return firstPageParam - 1
            },
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      // Started at page 3, should have previous pages
      expect(result.current.hasPreviousPage).toBe(true)
    })
  })

  describe("select function", () => {
    it("should apply select transformation to infinite data", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: () => undefined,
            select: (data) => ({
              ...data,
              pages: data.pages.map((page) =>
                page.map((post) => post.toUpperCase())
              ),
            }),
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      expect(result.current.data?.pages[0][0]).toBe("POST 1")
    })
  })

  describe("refetching", () => {
    it("should support manual refetch", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: () => undefined,
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      const initialCallCount = (
        mockReactor.callMethod as ReturnType<typeof vi.fn>
      ).mock.calls.length

      await act(async () => {
        await result.current.refetch()
      })

      expect(
        (mockReactor.callMethod as ReturnType<typeof vi.fn>).mock.calls.length
      ).toBeGreaterThan(initialCallCount)
    })
  })

  describe("maxPages option", () => {
    it("should accept maxPages option", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: (lastPage, allPages) => {
              if (lastPage.length < 10) return undefined
              return allPages.length + 1
            },
            maxPages: 2,
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      // Initial page should be loaded
      expect(result.current.data?.pages).toHaveLength(1)
      // maxPages option should be respected by tanstack query
      expect(result.current.hasNextPage).toBe(true)
    })
  })

  describe("data structure", () => {
    it("should return data in InfiniteData format", async () => {
      const { result } = renderHook(
        () =>
          useActorInfiniteQuery({
            reactor: mockReactor,
            functionName: "getPosts",
            initialPageParam: 1,
            getArgs: (page) => [{ page, limit: 10 }] as const,
            getNextPageParam: (lastPage, allPages) => {
              if (lastPage.length < 10) return undefined
              return allPages.length + 1
            },
          }),
        { wrapper }
      )

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true)
      })

      // Data should have pages array and pageParams array
      expect(result.current.data).toBeDefined()
      expect(Array.isArray(result.current.data?.pages)).toBe(true)
      expect(Array.isArray(result.current.data?.pageParams)).toBe(true)

      // Initial page should be loaded
      expect(result.current.data?.pages).toHaveLength(1)
      expect(result.current.data?.pages[0]).toHaveLength(10)

      // Pages can be flattened
      const allPosts = result.current.data?.pages.flat()
      expect(allPosts).toHaveLength(10)
      expect(allPosts?.[0]).toBe("Post 1")
    })
  })
})
