import { describe, it, expect, vi, beforeEach } from "vitest"
import * as Sentry from "@sentry/nextjs"
import { withTiming } from "../server-action-timing"

describe("withTiming", () => {
  let mockSetAttribute: ReturnType<typeof vi.fn>

  beforeEach(() => {
    mockSetAttribute = vi.fn()
    vi.mocked(Sentry.startSpan).mockImplementation((_opts, callback) =>
      callback({ setAttribute: mockSetAttribute } as any),
    )
  })

  describe("successful execution", () => {
    it("returns the wrapped function's result", async () => {
      const inner = vi.fn().mockResolvedValue({ data: "hello" })
      const wrapped = withTiming("test-action", inner)

      const result = await wrapped("arg1")
      expect(result).toEqual({ data: "hello" })
    })

    it("passes arguments through to the wrapped function", async () => {
      const inner = vi.fn().mockResolvedValue(null)
      const wrapped = withTiming("test-action", inner)

      await wrapped("a", 42, true)
      expect(inner).toHaveBeenCalledWith("a", 42, true)
    })

    it("creates a Sentry span with correct name and op", async () => {
      const wrapped = withTiming("myAction", vi.fn().mockResolvedValue(null))
      await wrapped()

      expect(Sentry.startSpan).toHaveBeenCalledWith(
        expect.objectContaining({
          name: "myAction",
          op: "server.action",
          attributes: { "server_action.name": "myAction" },
        }),
        expect.any(Function),
      )
    })

    it("sets duration_ms attribute on span", async () => {
      const nowSpy = vi.spyOn(performance, "now")
      nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(250)

      const wrapped = withTiming("test", vi.fn().mockResolvedValue(null))
      await wrapped()

      expect(mockSetAttribute).toHaveBeenCalledWith(
        "server_action.duration_ms",
        250,
      )
      nowSpy.mockRestore()
    })
  })

  describe("slow action detection", () => {
    it("logs console.warn when duration exceeds 1000ms", async () => {
      const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
      const nowSpy = vi.spyOn(performance, "now")
      nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(1500)

      const wrapped = withTiming("slowAction", vi.fn().mockResolvedValue(null))
      await wrapped()

      expect(warnSpy).toHaveBeenCalledWith(
        expect.stringContaining("slowAction"),
      )
      expect(warnSpy).toHaveBeenCalledWith(
        expect.stringContaining("1500"),
      )

      warnSpy.mockRestore()
      nowSpy.mockRestore()
    })

    it("sets server_action.slow attribute when slow", async () => {
      vi.spyOn(console, "warn").mockImplementation(() => {})
      const nowSpy = vi.spyOn(performance, "now")
      nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(2000)

      const wrapped = withTiming("slow", vi.fn().mockResolvedValue(null))
      await wrapped()

      expect(mockSetAttribute).toHaveBeenCalledWith("server_action.slow", true)

      nowSpy.mockRestore()
    })

    it("does not warn for fast actions", async () => {
      const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
      const nowSpy = vi.spyOn(performance, "now")
      nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(500)

      const wrapped = withTiming("fast", vi.fn().mockResolvedValue(null))
      await wrapped()

      expect(warnSpy).not.toHaveBeenCalled()

      warnSpy.mockRestore()
      nowSpy.mockRestore()
    })
  })

  describe("error handling", () => {
    it("re-throws the original error", async () => {
      const error = new Error("boom")
      vi.spyOn(console, "error").mockImplementation(() => {})
      const wrapped = withTiming("fail", vi.fn().mockRejectedValue(error))

      await expect(wrapped()).rejects.toThrow("boom")
    })

    it("calls Sentry.captureException with error and tags", async () => {
      const error = new Error("db failed")
      vi.spyOn(console, "error").mockImplementation(() => {})
      const wrapped = withTiming("dbAction", vi.fn().mockRejectedValue(error))

      await expect(wrapped()).rejects.toThrow()

      expect(Sentry.captureException).toHaveBeenCalledWith(error, {
        tags: { server_action: "dbAction" },
        extra: { duration_ms: expect.any(Number) },
      })
    })

    it("logs error with duration via console.error", async () => {
      const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
      const nowSpy = vi.spyOn(performance, "now")
      nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(300)

      const wrapped = withTiming("errAction", vi.fn().mockRejectedValue(new Error("oops")))
      await expect(wrapped()).rejects.toThrow()

      expect(errorSpy).toHaveBeenCalledWith(
        expect.stringContaining("errAction"),
        expect.any(Error),
      )

      errorSpy.mockRestore()
      nowSpy.mockRestore()
    })
  })
})
