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

import type { RequestOptions } from "../types"
import type {
  ErrorInterceptor,
  RequestInterceptor,
  ResponseInterceptor,
} from "./interceptors"
import { commonInterceptors, InterceptorManager } from "./interceptors"

describe("InterceptorManager", () => {
  let manager: InterceptorManager

  beforeEach(() => {
    manager = new InterceptorManager()
  })

  describe("Request Interceptors", () => {
    it("should add and execute request interceptor", async () => {
      const interceptor: RequestInterceptor = vi.fn().mockReturnValue({
        url: "https://modified.com",
        options: { method: "POST" },
      })

      manager.addRequestInterceptor(interceptor)

      const result = await manager.processRequest("https://original.com", {
        method: "GET",
      })

      expect(interceptor).toHaveBeenCalledWith({
        url: "https://original.com",
        options: { method: "GET" },
      })
      expect(result).toEqual({
        url: "https://modified.com",
        options: { method: "POST" },
      })
    })

    it("should execute multiple request interceptors in order", async () => {
      const interceptor1: RequestInterceptor = vi.fn().mockReturnValue({
        url: "https://step1.com",
        options: { method: "POST" },
      })
      const interceptor2: RequestInterceptor = vi.fn().mockReturnValue({
        url: "https://step2.com",
        options: { method: "PUT" },
      })

      manager.addRequestInterceptor(interceptor1)
      manager.addRequestInterceptor(interceptor2)

      const result = await manager.processRequest("https://original.com", {
        method: "GET",
      })

      expect(interceptor1).toHaveBeenCalledWith({
        url: "https://original.com",
        options: { method: "GET" },
      })
      expect(interceptor2).toHaveBeenCalledWith({
        url: "https://step1.com",
        options: { method: "POST" },
      })
      expect(result).toEqual({
        url: "https://step2.com",
        options: { method: "PUT" },
      })
    })

    it("should remove request interceptor", async () => {
      const interceptor: RequestInterceptor = vi.fn().mockReturnValue({
        url: "https://modified.com",
        options: { method: "POST" },
      })

      const remove = manager.addRequestInterceptor(interceptor)
      remove()

      const result = await manager.processRequest("https://original.com", {
        method: "GET",
      })

      expect(interceptor).not.toHaveBeenCalled()
      expect(result).toEqual({
        url: "https://original.com",
        options: { method: "GET" },
      })
    })

    it("should handle async request interceptors", async () => {
      const interceptor: RequestInterceptor = vi.fn().mockResolvedValue({
        url: "https://async.com",
        options: { method: "PATCH" },
      })

      manager.addRequestInterceptor(interceptor)

      const result = await manager.processRequest("https://original.com", {
        method: "GET",
      })

      expect(result).toEqual({
        url: "https://async.com",
        options: { method: "PATCH" },
      })
    })

    it("should handle interceptor that returns undefined", async () => {
      // @ts-expect-error
      const interceptor: RequestInterceptor = vi.fn().mockReturnValue()

      manager.addRequestInterceptor(interceptor)

      const result = await manager.processRequest("https://original.com", {
        method: "GET",
      })

      expect(result).toEqual({
        url: "https://original.com",
        options: { method: "GET" },
      })
    })
  })

  describe("Response Interceptors", () => {
    it("should add and execute response interceptor", async () => {
      const originalResponse = new Response("original")
      const modifiedResponse = new Response("modified")
      const interceptor: ResponseInterceptor = vi
        .fn()
        .mockReturnValue(modifiedResponse)

      manager.addResponseInterceptor(interceptor)

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: originalResponse,
      })
      expect(result).toBe(modifiedResponse)
    })

    it("should execute multiple response interceptors in order", async () => {
      const originalResponse = new Response("original")
      const response1 = new Response("step1")
      const response2 = new Response("step2")

      const interceptor1: ResponseInterceptor = vi
        .fn()
        .mockReturnValue(response1)
      const interceptor2: ResponseInterceptor = vi
        .fn()
        .mockReturnValue(response2)

      manager.addResponseInterceptor(interceptor1)
      manager.addResponseInterceptor(interceptor2)

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor1).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: originalResponse,
      })
      expect(interceptor2).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: response1,
      })
      expect(result).toBe(response2)
    })

    it("should remove response interceptor", async () => {
      const originalResponse = new Response("original")
      const interceptor: ResponseInterceptor = vi
        .fn()
        .mockReturnValue(new Response("modified"))

      const remove = manager.addResponseInterceptor(interceptor)
      remove()

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).not.toHaveBeenCalled()
      expect(result).toBe(originalResponse)
    })

    it("should handle async response interceptors", async () => {
      const originalResponse = new Response("original")
      const asyncResponse = new Response("async")
      const interceptor: ResponseInterceptor = vi
        .fn()
        .mockResolvedValue(asyncResponse)

      manager.addResponseInterceptor(interceptor)

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(result).toBe(asyncResponse)
    })

    it("should not modify response when interceptor returns undefined", async () => {
      const originalResponse = new Response("original")
      const interceptor: ResponseInterceptor = vi.fn().mockReturnValue(void 0)

      manager.addResponseInterceptor(interceptor)

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: originalResponse,
      })
      expect(result).toBe(originalResponse)
    })

    it("should not modify response when interceptor returns non-Response object", async () => {
      const originalResponse = new Response("original")
      const interceptor = vi.fn().mockReturnValue({ not: "a response" }) as ResponseInterceptor

      manager.addResponseInterceptor(interceptor)

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(result).toBe(originalResponse)
    })

    it("should not modify response when interceptor returns null", async () => {
      const originalResponse = new Response("original")
      const interceptor = vi.fn().mockReturnValue(null) as ResponseInterceptor

      manager.addResponseInterceptor(interceptor)

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(result).toBe(originalResponse)
    })

    it("should handle mixed interceptors with some returning undefined", async () => {
      const originalResponse = new Response("original")
      const modifiedResponse = new Response("modified")

      const interceptor1: ResponseInterceptor = vi.fn().mockReturnValue(void 0)
      const interceptor2: ResponseInterceptor = vi.fn().mockReturnValue(modifiedResponse)
      const interceptor3: ResponseInterceptor = vi.fn().mockReturnValue(void 0)

      manager.addResponseInterceptor(interceptor1)
      manager.addResponseInterceptor(interceptor2)
      manager.addResponseInterceptor(interceptor3)

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor1).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: originalResponse,
      })
      expect(interceptor2).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: originalResponse, // Should still get original since interceptor1 returned undefined
      })
      expect(interceptor3).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: modifiedResponse, // Should get modified response from interceptor2
      })
      expect(result).toBe(modifiedResponse)
    })

    it("should properly handle instanceof Response check with async interceptors", async () => {
      const originalResponse = new Response("original")
      const modifiedResponse = new Response("modified")

      const interceptor1: ResponseInterceptor = vi.fn().mockResolvedValue(void 0)
      const interceptor2: ResponseInterceptor = vi.fn().mockResolvedValue(modifiedResponse)

      manager.addResponseInterceptor(interceptor1)
      manager.addResponseInterceptor(interceptor2)

      const result = await manager.processResponse(
        originalResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(result).toBe(modifiedResponse)
    })
  })

  describe("Error Interceptors", () => {
    it("should add and execute error interceptor", async () => {
      const originalError = new Error("original")
      const modifiedError = new Error("modified")
      const interceptor: ErrorInterceptor = vi
        .fn()
        .mockReturnValue(modifiedError)

      manager.addErrorInterceptor(interceptor)

      const result = await manager.processError(
        originalError,
        null,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error: originalError,
      })
      expect(result).toBe(modifiedError)
    })

    it("should add and execute error interceptor with response parameter", async () => {
      const originalError = new Error("original")
      const modifiedError = new Error("modified")
      const mockResponse = new Response("error response", { status: 500 })
      const interceptor: ErrorInterceptor = vi
        .fn()
        .mockReturnValue(modifiedError)

      manager.addErrorInterceptor(interceptor)

      const result = await manager.processError(
        originalError,
        mockResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: mockResponse,
        error: originalError,
      })
      expect(result).toBe(modifiedError)
    })

    it("should handle null response parameter", async () => {
      const originalError = new Error("original")
      const interceptor: ErrorInterceptor = vi.fn().mockReturnValue(originalError)

      manager.addErrorInterceptor(interceptor)

      const result = await manager.processError(
        originalError,
        null,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error: originalError,
      })
      expect(result).toBe(originalError)
    })

    it("should execute multiple error interceptors in order", async () => {
      const originalError = new Error("original")
      const error1 = new Error("step1")
      const error2 = new Error("step2")

      const interceptor1: ErrorInterceptor = vi.fn().mockReturnValue(error1)
      const interceptor2: ErrorInterceptor = vi.fn().mockReturnValue(error2)

      manager.addErrorInterceptor(interceptor1)
      manager.addErrorInterceptor(interceptor2)

      const result = await manager.processError(
        originalError,
        null,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor1).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error: originalError,
      })
      expect(interceptor2).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error: error1,
      })
      expect(result).toBe(error2)
    })

    it("should remove error interceptor", async () => {
      const originalError = new Error("original")
      const interceptor: ErrorInterceptor = vi
        .fn()
        .mockReturnValue(new Error("modified"))

      const remove = manager.addErrorInterceptor(interceptor)
      remove()

      const result = await manager.processError(
        originalError,
        null,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).not.toHaveBeenCalled()
      expect(result).toBe(originalError)
    })

    it("should handle error interceptor that returns undefined", async () => {
      const originalError = new Error("original")
      const mockResponse = new Response("error response", { status: 500 })
      // @ts-expect-error
      const interceptor: ErrorInterceptor = vi.fn().mockReturnValue()

      manager.addErrorInterceptor(interceptor)

      const result = await manager.processError(
        originalError,
        mockResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: mockResponse,
        error: originalError,
      })
      expect(result).toBeUndefined()
    })

    it("should handle async error interceptors with response", async () => {
      const originalError = new Error("original")
      const asyncError = new Error("async")
      const mockResponse = new Response("error response", { status: 503 })
      const interceptor: ErrorInterceptor = vi
        .fn()
        .mockResolvedValue(asyncError)

      manager.addErrorInterceptor(interceptor)

      const result = await manager.processError(
        originalError,
        mockResponse,
        "https://test.com",
        { method: "GET" },
      )

      expect(interceptor).toHaveBeenCalledWith({
        url: "https://test.com",
        options: { method: "GET" },
        response: mockResponse,
        error: originalError,
      })
      expect(result).toBe(asyncError)
    })
  })

  describe("Clear Interceptors", () => {
    it("should clear all interceptors", async () => {
      const requestInterceptor: RequestInterceptor = vi.fn().mockReturnValue({
        url: "https://modified.com",
        options: { method: "POST" },
      })
      const responseInterceptor: ResponseInterceptor = vi
        .fn()
        .mockReturnValue(new Response("modified"))
      const errorInterceptor: ErrorInterceptor = vi
        .fn()
        .mockReturnValue(new Error("modified"))

      manager.addRequestInterceptor(requestInterceptor)
      manager.addResponseInterceptor(responseInterceptor)
      manager.addErrorInterceptor(errorInterceptor)

      manager.clear()

      // Test that interceptors are no longer called
      const requestResult = await manager.processRequest("https://test.com", {
        method: "GET",
      })
      const responseResult = await manager.processResponse(
        new Response("original"),
        "https://test.com",
        { method: "GET" },
      )
      const errorResult = await manager.processError(
        new Error("original"),
        null,
        "https://test.com",
        { method: "GET" },
      )

      expect(requestInterceptor).not.toHaveBeenCalled()
      expect(responseInterceptor).not.toHaveBeenCalled()
      expect(errorInterceptor).not.toHaveBeenCalled()

      expect(requestResult).toEqual({
        url: "https://test.com",
        options: { method: "GET" },
      })
      expect(responseResult).toBeInstanceOf(Response)
      expect(errorResult).toBeInstanceOf(Error)
    })
  })
})

describe("Common Interceptors", () => {
  describe("addAuthToken", () => {
    it("should add authorization header to request", () => {
      const interceptor = commonInterceptors.addAuthToken("test-token")
      const options: RequestOptions = {
        method: "GET",
        headers: { "X-Custom": "value" },
      }

      const result = interceptor({ url: "https://test.com", options })

      expect(result).toEqual({
        url: "https://test.com",
        options: {
          method: "GET",
          headers: {
            "X-Custom": "value",
            "Authorization": "Bearer test-token",
          },
        },
      })
    })

    it("should handle request without existing headers", () => {
      const interceptor = commonInterceptors.addAuthToken("test-token")
      const options: RequestOptions = { method: "GET" }

      const result = interceptor({ url: "https://test.com", options })

      expect(result).toEqual({
        url: "https://test.com",
        options: {
          method: "GET",
          headers: {
            Authorization: "Bearer test-token",
          },
        },
      })
    })
  })

  describe("logRequests", () => {
    it("should log request details", () => {
      const mockLogger = { log: vi.fn() }
      const interceptor = commonInterceptors.logRequests(mockLogger)

      const result = interceptor({ url: "https://test.com", options: { method: "POST" } })

      expect(mockLogger.log).toHaveBeenCalledWith(
        "Request: POST https://test.com",
      )
      expect(result).toEqual({
        url: "https://test.com",
        options: { method: "POST" },
      })
    })

    it("should default to GET method in logs", () => {
      const mockLogger = { log: vi.fn() }
      const interceptor = commonInterceptors.logRequests(mockLogger)

      interceptor({ url: "https://test.com", options: {} })

      expect(mockLogger.log).toHaveBeenCalledWith(
        "Request: GET https://test.com",
      )
    })
  })

  describe("logResponses", () => {
    it("should log response details", () => {
      const mockLogger = { log: vi.fn() }
      const interceptor = commonInterceptors.logResponses(mockLogger)
      const response = new Response("test", { status: 200 })

      const result = interceptor({
        url: "https://test.com",
        options: { method: "POST" },
        response,
      })

      expect(mockLogger.log).toHaveBeenCalledWith(
        "Response: 200 POST https://test.com",
      )
      expect(result).toBe(response)
    })

    it("should default to GET method in logs", () => {
      const mockLogger = { log: vi.fn() }
      const interceptor = commonInterceptors.logResponses(mockLogger)
      const response = new Response("test", { status: 404 })

      interceptor({
        url: "https://test.com",
        options: {},
        response,
      })

      expect(mockLogger.log).toHaveBeenCalledWith(
        "Response: 404 GET https://test.com",
      )
    })
  })

  describe("retryOnError", () => {
    it("should retry on error up to max retries", async () => {
      const interceptor = commonInterceptors.retryOnError(2, 10)
      const error = new Error("test error")

      // First call - should retry (return undefined)
      const result1 = await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error,
      })
      expect(result1).toBeUndefined()

      // Second call - should retry (return undefined)
      const result2 = await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error,
      })
      expect(result2).toBeUndefined()

      // Third call - should return error (max retries exceeded)
      const result3 = await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error,
      })
      expect(result3).toBe(error)
    })

    it("should use default retry settings", async () => {
      // Use shorter delay to avoid test timeout
      const interceptor = commonInterceptors.retryOnError(3, 10)
      const error = new Error("test error")

      // Should retry 3 times by default
      for (let i = 0; i < 3; i++) {
        const result = await interceptor({
          url: "https://test.com",
          options: { method: "GET" },
          response: null,
          error,
        })
        expect(result).toBeUndefined()
      }

      // Fourth call should return error
      const result = await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error,
      })
      expect(result).toBe(error)
    })

    it("should handle different errors independently", async () => {
      const interceptor = commonInterceptors.retryOnError(1, 10)
      const error1 = new Error("error 1")
      const error2 = new Error("error 2")

      // First error - should retry
      const result1 = await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error: error1,
      })
      expect(result1).toBeUndefined()

      // Second error - should retry (independent counter)
      const result2 = await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error: error2,
      })
      expect(result2).toBeUndefined()

      // First error again - should return error (max retries exceeded)
      const result3 = await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error: error1,
      })
      expect(result3).toBe(error1)

      // Second error again - should return error (max retries exceeded)
      const result4 = await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error: error2,
      })
      expect(result4).toBe(error2)
    })

    it("should wait before retry with exponential backoff", async () => {
      const interceptor = commonInterceptors.retryOnError(2, 100)
      const error = new Error("test error")

      const start = Date.now()

      // First retry
      await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error,
      })
      const firstRetryTime = Date.now() - start

      // Second retry
      await interceptor({
        url: "https://test.com",
        options: { method: "GET" },
        response: null,
        error,
      })
      const secondRetryTime = Date.now() - start

      // Should have waited at least 100ms for first retry and 200ms for second
      expect(firstRetryTime).toBeGreaterThanOrEqual(90)
      expect(secondRetryTime).toBeGreaterThanOrEqual(290) // 100ms + 200ms
    })
  })
})

describe("Interceptor Integration", () => {
  it("should work with all interceptor types together", async () => {
    const manager = new InterceptorManager()
    const mockLogger = { log: vi.fn() }

    // Add interceptors
    manager.addRequestInterceptor(commonInterceptors.addAuthToken("token"))
    manager.addRequestInterceptor(commonInterceptors.logRequests(mockLogger))
    manager.addResponseInterceptor(commonInterceptors.logResponses(mockLogger))
    manager.addErrorInterceptor(commonInterceptors.retryOnError(1, 10))

    // Test request processing
    const requestResult = await manager.processRequest("https://test.com", {
      method: "POST",
    })

    expect(requestResult.options.headers).toEqual({
      Authorization: "Bearer token",
    })
    expect(mockLogger.log).toHaveBeenCalledWith(
      "Request: POST https://test.com",
    )

    // Test response processing
    const response = new Response("test", { status: 200 })
    const responseResult = await manager.processResponse(
      response,
      "https://test.com",
      { method: "POST" },
    )

    expect(responseResult).toBe(response)
    expect(mockLogger.log).toHaveBeenCalledWith(
      "Response: 200 POST https://test.com",
    )

    // Test error processing
    const error = new Error("test error")
    const errorResult = await manager.processError(error, null, "https://test.com", {
      method: "POST",
    })

    expect(errorResult).toBeUndefined() // Should retry
  })

  it("should handle interceptor removal in complex scenarios", async () => {
    const manager = new InterceptorManager()
    const mockLogger = { log: vi.fn() }

    const removeAuth = manager.addRequestInterceptor(
      commonInterceptors.addAuthToken("token"),
    )
    const removeLog = manager.addRequestInterceptor(
      commonInterceptors.logRequests(mockLogger),
    )

    // Test with all interceptors
    let result = await manager.processRequest("https://test.com", {
      method: "GET",
    })
    expect(result.options.headers).toEqual({ Authorization: "Bearer token" })
    expect(mockLogger.log).toHaveBeenCalledWith(
      "Request: GET https://test.com",
    )

    // Remove auth interceptor
    removeAuth()
    mockLogger.log.mockClear()

    result = await manager.processRequest("https://test.com", {
      method: "GET",
    })
    expect(result.options.headers).toBeUndefined()
    expect(mockLogger.log).toHaveBeenCalledWith(
      "Request: GET https://test.com",
    )

    // Remove log interceptor
    removeLog()
    mockLogger.log.mockClear()

    result = await manager.processRequest("https://test.com", {
      method: "GET",
    })
    expect(result.options.headers).toBeUndefined()
    expect(mockLogger.log).not.toHaveBeenCalled()
  })
})
