import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import { Request, Response } from "express"
import {
  stripeWebhookHandler,
  handleCheckoutCompleted,
  handleSubscriptionUpdate,
  handleSubscriptionCancellation,
  handleFailedPayment,
  setStripeWebhookDependencies,
  resetStripeWebhookDependencies,
} from "../stripe-webhook"

describe("Stripe Webhook Handler", () => {
  let mockReq: Partial<Request>
  let mockRes: Partial<Response>
  let mockDependencies: any

  beforeEach(() => {
    // Setup request mock
    mockReq = {
      headers: {
        "stripe-signature": "test-signature",
      },
      body: "test-body",
    }

    // Setup response mock
    mockRes = {
      status: jest.fn().mockReturnThis() as jest.MockedFunction<(code: number) => Response>,
      send: jest.fn() as jest.MockedFunction<(body?: any) => Response>,
      json: jest.fn() as jest.MockedFunction<(body?: any) => Response>,
    }

    // Setup mock dependencies
    mockDependencies = {
      stripe: {
        webhooks: {
          constructEvent: jest.fn(),
        },
        subscriptions: {
          update: jest.fn(),
          retrieve: jest.fn(),
        },
        customers: {
          update: jest.fn(),
          retrieve: jest.fn(),
        },
        prices: {
          retrieve: jest.fn(),
        },
      },
      prisma: {
        subscriptions: {
          upsert: jest.fn(),
          update: jest.fn(),
        },
      },
      Sentry: {
        captureException: jest.fn(),
      },
      getWebhookSecret: jest.fn().mockReturnValue("test-webhook-secret"),
    }

    setStripeWebhookDependencies(mockDependencies)
  })

  afterEach(() => {
    resetStripeWebhookDependencies()
    jest.clearAllMocks()
  })

  describe("stripeWebhookHandler", () => {
    describe("webhook event construction", () => {
      it("should return 400 when webhook secret is not configured", async () => {
        ;(mockDependencies.getWebhookSecret as jest.MockedFunction<any>).mockReturnValue(undefined)

        await expect(
          stripeWebhookHandler(mockReq as Request, mockRes as Response),
        ).rejects.toMatchObject({
          message: expect.stringContaining("STRIPE_WEBHOOK_SECRET is not configured"),
        })
        expect(mockDependencies.Sentry.captureException).toHaveBeenCalled()
        expect(mockRes.status).not.toHaveBeenCalled()
      })

      it("should return 400 when webhook signature verification fails", async () => {
        const error = new Error("Invalid signature")
        ;(
          mockDependencies.stripe.webhooks.constructEvent as jest.MockedFunction<any>
        ).mockImplementation(() => {
          throw error
        })

        await expect(
          stripeWebhookHandler(mockReq as Request, mockRes as Response),
        ).rejects.toMatchObject({
          message: expect.stringContaining("Invalid signature"),
        })

        expect(mockDependencies.stripe.webhooks.constructEvent).toHaveBeenCalledWith(
          "test-body",
          "test-signature",
          "test-webhook-secret",
        )
        expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
        expect(mockRes.status).not.toHaveBeenCalled()
      })

      it("should process webhook event successfully", async () => {
        const mockEvent = {
          id: "evt_test123",
          type: "customer.subscription.created",
          data: {
            object: {
              id: "sub_test123",
              customer: "cus_test123",
              metadata: { userId: "user_test123" },
              items: {
                data: [
                  {
                    price: { id: "price_test123" },
                  },
                ],
              },
              status: "active",
              current_period_start: 1640995200,
              current_period_end: 1643673600,
            },
          },
        }

        ;(
          mockDependencies.stripe.webhooks.constructEvent as jest.MockedFunction<any>
        ).mockReturnValue(mockEvent)
        ;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
          metadata: { tier: "pro", optimizations: "1000" },
        })

        await stripeWebhookHandler(mockReq as Request, mockRes as Response)

        expect(mockRes.json).toHaveBeenCalledWith({ received: true })
      })

      it("should handle unimplemented event types", async () => {
        const mockEvent = {
          id: "evt_test123",
          type: "unknown.event.type",
          data: {
            object: {},
          },
        }

        ;(
          mockDependencies.stripe.webhooks.constructEvent as jest.MockedFunction<any>
        ).mockReturnValue(mockEvent)

        await stripeWebhookHandler(mockReq as Request, mockRes as Response)

        expect(mockRes.json).toHaveBeenCalledWith({ received: true })
      })
    })
  })

  describe("handleCheckoutCompleted", () => {
    it("should handle checkout session without userId metadata", async () => {
      const session = {
        id: "cs_test123",
        metadata: {},
        mode: "subscription",
      }

      await handleCheckoutCompleted(session)

      expect(mockDependencies.stripe.subscriptions.update).not.toHaveBeenCalled()
    })

    it("should handle non-subscription checkout", async () => {
      const session = {
        id: "cs_test123",
        metadata: { userId: "user_test123" },
        mode: "payment",
      }

      await handleCheckoutCompleted(session)

      expect(mockDependencies.stripe.subscriptions.update).not.toHaveBeenCalled()
    })

    it("should update subscription and customer metadata", async () => {
      const session = {
        id: "cs_test123",
        metadata: { userId: "user_test123" },
        mode: "subscription",
        subscription: "sub_test123",
        customer: "cus_test123",
      }

      ;(
        mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
      ).mockResolvedValue({
        id: "sub_test123",
        customer: "cus_test123",
        metadata: { userId: "user_test123" },
        items: {
          data: [
            {
              price: { id: "price_test123" },
            },
          ],
        },
        status: "active",
        current_period_start: 1640995200,
        current_period_end: 1643673600,
      })
      ;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: { tier: "pro", optimizations: "1000" },
      })

      await handleCheckoutCompleted(session)

      expect(mockDependencies.stripe.subscriptions.update).toHaveBeenCalledWith("sub_test123", {
        metadata: { userId: "user_test123" },
      })
      expect(mockDependencies.stripe.customers.update).toHaveBeenCalledWith("cus_test123", {
        metadata: { userId: "user_test123" },
      })
      expect(mockDependencies.stripe.subscriptions.retrieve).toHaveBeenCalledWith("sub_test123")
      expect(mockDependencies.prisma.subscriptions.upsert).toHaveBeenCalled()
    })

    it("should handle errors during checkout processing", async () => {
      const session = {
        id: "cs_test123",
        metadata: { userId: "user_test123" },
        mode: "subscription",
        subscription: "sub_test123",
        customer: "cus_test123",
      }

      const error = new Error("Stripe API error")
      ;(mockDependencies.stripe.subscriptions.update as jest.MockedFunction<any>).mockRejectedValue(
        error,
      )

      await handleCheckoutCompleted(session)

      expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
    })
  })

  describe("handleSubscriptionUpdate", () => {
    it("should handle subscription without userId in metadata", async () => {
      const subscription = {
        id: "sub_test123",
        customer: "cus_test123",
        metadata: {},
      }

      ;(mockDependencies.stripe.customers.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: {},
      })

      await handleSubscriptionUpdate(subscription)

      expect(mockDependencies.stripe.customers.retrieve).toHaveBeenCalledWith("cus_test123")
      expect(mockDependencies.prisma.subscriptions.upsert).not.toHaveBeenCalled()
    })

    it("should get userId from customer metadata when not in subscription", async () => {
      const subscription = {
        id: "sub_test123",
        customer: "cus_test123",
        metadata: {},
        items: {
          data: [
            {
              price: { id: "price_test123" },
            },
          ],
        },
        status: "active",
        current_period_start: 1640995200,
        current_period_end: 1643673600,
      }

      ;(mockDependencies.stripe.customers.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: { userId: "user_test123" },
      })
      ;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: { tier: "enterprise", optimizations: "2000" },
      })

      await handleSubscriptionUpdate(subscription)

      expect(mockDependencies.stripe.customers.retrieve).toHaveBeenCalledWith("cus_test123")
      expect(mockDependencies.stripe.prices.retrieve).toHaveBeenCalledWith("price_test123")
      expect(mockDependencies.prisma.subscriptions.upsert).toHaveBeenCalled()
    })

    it("should handle customer retrieval error gracefully", async () => {
      const subscription = {
        id: "sub_test123",
        customer: "cus_test123",
        metadata: {},
      }

      const error = new Error("Customer not found")
      ;(mockDependencies.stripe.customers.retrieve as jest.MockedFunction<any>).mockRejectedValue(
        error,
      )

      await handleSubscriptionUpdate(subscription)

      expect(mockDependencies.prisma.subscriptions.upsert).not.toHaveBeenCalled()
    })

    it("should create new subscription record", async () => {
      const subscription = {
        id: "sub_test123",
        customer: "cus_test123",
        metadata: { userId: "user_test123" },
        items: {
          data: [
            {
              price: { id: "price_test123" },
            },
          ],
        },
        status: "active",
        current_period_start: 1640995200,
        current_period_end: 1643673600,
        cancel_at_period_end: false,
      }

      ;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: { tier: "pro", optimizations: "1000" },
      })

      await handleSubscriptionUpdate(subscription)

      expect(mockDependencies.stripe.prices.retrieve).toHaveBeenCalledWith("price_test123")
      expect(mockDependencies.prisma.subscriptions.upsert).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { user_id: "user_test123" },
          create: expect.objectContaining({
            user_id: "user_test123",
            stripe_customer_id: "cus_test123",
            stripe_subscription_id: "sub_test123",
            plan_type: "pro",
            optimizations_limit: 1000,
            optimizations_used: 0,
            subscription_status: "active",
            cancel_at_period_end: false,
            cancellation_request_date: null,
          }),
          update: expect.any(Object),
        }),
      )
    })

    it("should handle cancel_at_period_end = true", async () => {
      const subscription = {
        id: "sub_test123",
        customer: "cus_test123",
        metadata: { userId: "user_test123" },
        items: {
          data: [
            {
              price: { id: "price_test123" },
            },
          ],
        },
        status: "active",
        current_period_start: 1640995200,
        current_period_end: 1643673600,
        cancel_at_period_end: true,
      }

      ;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: { tier: "pro", optimizations: "1000" },
      })

      await handleSubscriptionUpdate(subscription)

      const upsertCall = (mockDependencies.prisma.subscriptions.upsert as jest.MockedFunction<any>)
        .mock.calls[0][0]
      expect(upsertCall.update.cancel_at_period_end).toBe(true)
      expect(upsertCall.update.cancellation_request_date).toBeInstanceOf(Date)
    })

    it("should handle cancel_at_period_end = false (reactivation)", async () => {
      const subscription = {
        id: "sub_test123",
        customer: "cus_test123",
        metadata: { userId: "user_test123" },
        items: {
          data: [
            {
              price: { id: "price_test123" },
            },
          ],
        },
        status: "active",
        current_period_start: 1640995200,
        current_period_end: 1643673600,
        cancel_at_period_end: false,
      }

      ;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: { tier: "pro", optimizations: "1000" },
      })

      await handleSubscriptionUpdate(subscription)

      const upsertCall = (mockDependencies.prisma.subscriptions.upsert as jest.MockedFunction<any>)
        .mock.calls[0][0]
      expect(upsertCall.update.cancel_at_period_end).toBe(false)
      expect(upsertCall.update.cancellation_request_date).toBe(null)
    })

    it("should use default values when price metadata is missing", async () => {
      const subscription = {
        id: "sub_test123",
        customer: "cus_test123",
        metadata: { userId: "user_test123" },
        items: {
          data: [
            {
              price: { id: "price_test123" },
            },
          ],
        },
        status: "active",
        current_period_start: 1640995200,
        current_period_end: 1643673600,
      }

      ;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: {},
      })

      await handleSubscriptionUpdate(subscription)

      const upsertCall = (mockDependencies.prisma.subscriptions.upsert as jest.MockedFunction<any>)
        .mock.calls[0][0]
      expect(upsertCall.create.plan_type).toBe("pro")
      expect(upsertCall.create.optimizations_limit).toBe(500)
    })

    it("should handle database errors", async () => {
      const subscription = {
        id: "sub_test123",
        customer: "cus_test123",
        metadata: { userId: "user_test123" },
        items: {
          data: [
            {
              price: { id: "price_test123" },
            },
          ],
        },
        status: "active",
        current_period_start: 1640995200,
        current_period_end: 1643673600,
      }

      const error = new Error("Database error")
      ;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
        metadata: { tier: "pro", optimizations: "1000" },
      })
      ;(mockDependencies.prisma.subscriptions.upsert as jest.MockedFunction<any>).mockRejectedValue(
        error,
      )

      await handleSubscriptionUpdate(subscription)

      expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
    })
  })

  describe("handleSubscriptionCancellation", () => {
    it("should handle subscription without userId", async () => {
      const subscription = {
        id: "sub_test123",
        metadata: {},
      }

      await handleSubscriptionCancellation(subscription)

      expect(mockDependencies.prisma.subscriptions.update).not.toHaveBeenCalled()
    })

    it("should cancel subscription successfully", async () => {
      const subscription = {
        id: "sub_test123",
        metadata: { userId: "user_test123" },
      }

      await handleSubscriptionCancellation(subscription)

      expect(mockDependencies.prisma.subscriptions.update).toHaveBeenCalledWith({
        where: { user_id: "user_test123" },
        data: {
          subscription_status: "canceled",
          plan_type: "free",
          optimizations_limit: 4000,
          stripe_subscription_id: null,
          cancel_at_period_end: false,
          cancellation_request_date: null,
        },
      })
    })

    it("should handle database errors during cancellation", async () => {
      const subscription = {
        id: "sub_test123",
        metadata: { userId: "user_test123" },
      }

      const error = new Error("Database error")
      ;(mockDependencies.prisma.subscriptions.update as jest.MockedFunction<any>).mockRejectedValue(
        error,
      )

      await handleSubscriptionCancellation(subscription)

      expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
    })
  })

  describe("handleFailedPayment", () => {
    it("should handle invoice without subscription", async () => {
      const invoice = {
        id: "in_test123",
      }

      await handleFailedPayment(invoice)

      expect(mockDependencies.stripe.subscriptions.retrieve).not.toHaveBeenCalled()
    })

    it("should handle subscription without userId", async () => {
      const invoice = {
        id: "in_test123",
        subscription: "sub_test123",
      }

      ;(
        mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
      ).mockResolvedValue({
        id: "sub_test123",
        metadata: {},
      })

      await handleFailedPayment(invoice)

      expect(mockDependencies.stripe.subscriptions.retrieve).toHaveBeenCalledWith("sub_test123")
      expect(mockDependencies.prisma.subscriptions.update).not.toHaveBeenCalled()
    })

    it("should update subscription to past_due", async () => {
      const invoice = {
        id: "in_test123",
        subscription: "sub_test123",
      }

      ;(
        mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
      ).mockResolvedValue({
        id: "sub_test123",
        metadata: { userId: "user_test123" },
      })

      await handleFailedPayment(invoice)

      expect(mockDependencies.stripe.subscriptions.retrieve).toHaveBeenCalledWith("sub_test123")
      expect(mockDependencies.prisma.subscriptions.update).toHaveBeenCalledWith({
        where: { user_id: "user_test123" },
        data: {
          subscription_status: "past_due",
        },
      })
    })

    it("should handle errors during failed payment processing", async () => {
      const invoice = {
        id: "in_test123",
        subscription: "sub_test123",
      }

      const error = new Error("Database error")
      ;(
        mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
      ).mockResolvedValue({
        id: "sub_test123",
        metadata: { userId: "user_test123" },
      })
      ;(mockDependencies.prisma.subscriptions.update as jest.MockedFunction<any>).mockRejectedValue(
        error,
      )

      await handleFailedPayment(invoice)

      expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
    })

    it("should handle Stripe API errors when retrieving subscription", async () => {
      const invoice = {
        id: "in_test123",
        subscription: "sub_test123",
      }

      const error = new Error("Subscription not found")
      ;(
        mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
      ).mockRejectedValue(error)

      await handleFailedPayment(invoice)

      expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
      expect(mockDependencies.prisma.subscriptions.update).not.toHaveBeenCalled()
    })
  })
})
