import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import { Request, Response } from "express"
import {
  handleSlackEvents,
  verifySlackRequest,
  setHandleSlackEventsDependencies,
  resetHandleSlackEventsDependencies,
} from "../slack-events"

// Helper function to create mock requests
function createMockRequest(options: { headers?: Record<string, string>; body?: any }): Request {
  return {
    headers: options.headers || {},
    body: options.body || {},
  } as unknown as Request
}

describe("handleSlackEvents", () => {
  let mockReq: Request
  let mockRes: Partial<Response>
  let mockDependencies: any
  let consoleLogSpy: ReturnType<typeof jest.spyOn>
  let consoleWarnSpy: ReturnType<typeof jest.spyOn>
  let consoleErrorSpy: ReturnType<typeof jest.spyOn>

  beforeEach(() => {
    // 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>,
      headersSent: false,
    }

    // Setup mock dependencies
    mockDependencies = {
      crypto: {
        createHmac: jest.fn(),
        timingSafeEqual: jest.fn(),
      },
      posthog: {
        capture: jest.fn(),
      },
      processReaction: jest.fn(),
      Sentry: {
        captureException: jest.fn(),
      },
      getSlackSigningSecret: jest.fn(),
      getCurrentTime: jest.fn(),
      console: {
        log: jest.fn(),
        warn: jest.fn(),
        error: jest.fn(),
      },
    }

    setHandleSlackEventsDependencies(mockDependencies)

    consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {})
    consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {})
    consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
  })

  afterEach(() => {
    resetHandleSlackEventsDependencies()
    jest.clearAllMocks()
    consoleLogSpy.mockRestore()
    consoleWarnSpy.mockRestore()
    consoleErrorSpy.mockRestore()
  })

  describe("verifySlackRequest", () => {
    let mockHmac: any

    beforeEach(() => {
      mockHmac = {
        update: jest.fn().mockReturnThis(),
        digest: jest.fn(),
      }
      ;(mockDependencies.crypto.createHmac as jest.MockedFunction<any>).mockReturnValue(mockHmac)
    })

    it("should return true when SLACK_SIGNING_SECRET is not configured", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        undefined,
      )

      const req = createMockRequest({
        headers: {},
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(true)
      expect(consoleWarnSpy).toHaveBeenCalledWith(
        "SLACK_SIGNING_SECRET not configured. Skipping request verification.",
      )
    })

    it("should return false when headers are missing", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )

      const req = createMockRequest({
        headers: {},
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(false)
    })

    it("should return false when signature header is missing", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )

      const req = createMockRequest({
        headers: {
          "x-slack-request-timestamp": "1234567890",
        },
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(false)
    })

    it("should return false when timestamp header is missing", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )

      const req = createMockRequest({
        headers: {
          "x-slack-signature": "v0=test-signature",
        },
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(false)
    })

    it("should return false when request is too old (replay attack prevention)", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )
      ;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(1000000000)

      const oldTimestamp = 1000000000 - 400 // 400 seconds old (> 300 limit)

      const req = createMockRequest({
        headers: {
          "x-slack-signature": "v0=test-signature",
          "x-slack-request-timestamp": oldTimestamp.toString(),
        },
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(false)
    })

    it("should verify signature correctly for valid request", () => {
      const currentTime = 1000000000
      const timestamp = currentTime

      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )
      ;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(currentTime)

      mockHmac.digest.mockReturnValue("valid-hash")
      ;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(true)

      const req = createMockRequest({
        headers: {
          "x-slack-signature": "v0=valid-signature",
          "x-slack-request-timestamp": timestamp.toString(),
        },
        body: { test: "data" },
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(true)
      expect(mockDependencies.crypto.createHmac).toHaveBeenCalledWith("sha256", "test-secret")
      expect(mockHmac.update).toHaveBeenCalledWith(`v0:${timestamp}:{"test":"data"}`)
      expect(mockHmac.digest).toHaveBeenCalledWith("hex")
      expect(mockDependencies.crypto.timingSafeEqual).toHaveBeenCalled()
    })

    it("should handle string body correctly", () => {
      const currentTime = 1000000000
      const timestamp = currentTime

      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )
      ;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(currentTime)

      mockHmac.digest.mockReturnValue("valid-hash")
      ;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(true)

      const req = createMockRequest({
        headers: {
          "x-slack-signature": "v0=valid-signature",
          "x-slack-request-timestamp": timestamp.toString(),
        },
        body: "string-body",
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(true)
      expect(mockHmac.update).toHaveBeenCalledWith(`v0:${timestamp}:string-body`)
    })

    it("should return false when signature verification fails", () => {
      const currentTime = 1000000000
      const timestamp = currentTime

      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )
      ;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(currentTime)

      mockHmac.digest.mockReturnValue("invalid-hash")
      ;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(false)

      const req = createMockRequest({
        headers: {
          "x-slack-signature": "v0=wrong-signature",
          "x-slack-request-timestamp": timestamp.toString(),
        },
        body: { test: "data" },
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(false)
    })

    it("should handle empty string signing secret", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue("")

      const req = createMockRequest({
        headers: {},
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(true)
      expect(consoleWarnSpy).toHaveBeenCalledWith(
        expect.stringContaining(
          "SLACK_SIGNING_SECRET not configured. Skipping request verification.",
        ),
      )
    })

    it("should handle null signing secret", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(null)

      const req = createMockRequest({
        headers: {},
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(true)
      expect(consoleWarnSpy).toHaveBeenCalledWith(
        expect.stringContaining(
          "SLACK_SIGNING_SECRET not configured. Skipping request verification.",
        ),
      )
    })

    it("should handle invalid timestamp format", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )
      ;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(1000000000)

      // Set up the crypto mocks properly
      mockHmac.digest.mockReturnValue("test-hash")
      ;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(false)

      const req = createMockRequest({
        headers: {
          "x-slack-signature": "v0=test-signature",
          "x-slack-request-timestamp": "invalid-timestamp",
        },
        body: {},
      })

      const result = verifySlackRequest(req)

      // Invalid timestamp should cause verification to fail
      // Even though the timestamp check might pass (due to NaN behavior),
      // the signature verification should fail
      expect(result).toBe(false)
    })
  })

  describe("handleSlackEvents", () => {
    beforeEach(() => {
      // Setup verifySlackRequest to return true by default
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )
      ;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(1000000000)

      const mockHmac = {
        update: jest.fn().mockReturnThis(),
        digest: jest.fn().mockReturnValue("valid-hash"),
      }
      ;(mockDependencies.crypto.createHmac as jest.MockedFunction<any>).mockReturnValue(mockHmac)
      ;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(true)
    })

    it("should throw forbidden exception when Slack request verification fails", async () => {
      ;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(false)

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "invalid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {},
      })

      await expect(handleSlackEvents(mockReq, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Access forbidden"),
      })

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("Failed to verify Slack request"),
      )
      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should handle URL verification challenge", async () => {
      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "url_verification",
          challenge: "test-challenge-string",
        },
      })

      await handleSlackEvents(mockReq, mockRes as Response)

      expect(consoleLogSpy).toHaveBeenCalledWith(
        expect.stringContaining("Handling Slack URL verification challenge"),
      )
      expect(mockRes.json).toHaveBeenCalledWith({ challenge: "test-challenge-string" })
    })

    it("should acknowledge receipt and process reaction_added event", async () => {
      const mockEvent = {
        type: "reaction_added",
        reaction: "white_check_mark",
        user: "U123456",
        item: {
          type: "message",
          channel: "C123456",
          ts: "1234567890.123456",
        },
      }

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "event_callback",
          event: mockEvent,
        },
      })
      ;(mockDependencies.processReaction as jest.MockedFunction<any>).mockResolvedValue(true)

      await handleSlackEvents(mockReq, mockRes as Response)

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockRes.send).toHaveBeenCalledWith("OK")
      expect(consoleLogSpy).toHaveBeenCalledWith(
        expect.stringContaining("Processing Slack event: reaction_added"),
      )
      expect(mockDependencies.processReaction).toHaveBeenCalledWith(mockEvent)
      expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
        distinctId: "system",
        event: "slack-approval-reaction-processed",
        properties: {
          reaction: "white_check_mark",
          result: "success",
        },
      })
    })

    it("should not capture analytics when processReaction returns false", async () => {
      const mockEvent = {
        type: "reaction_added",
        reaction: "x",
        user: "U123456",
      }

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "event_callback",
          event: mockEvent,
        },
      })
      ;(mockDependencies.processReaction as jest.MockedFunction<any>).mockResolvedValue(false)

      await handleSlackEvents(mockReq, mockRes as Response)

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockDependencies.processReaction).toHaveBeenCalledWith(mockEvent)
      expect(mockDependencies.posthog.capture).not.toHaveBeenCalledWith(
        expect.objectContaining({
          event: "slack-approval-reaction-processed",
        }),
      )
    })

    it("should ignore non-reaction events", async () => {
      const mockEvent = {
        type: "message",
        text: "Hello world",
      }

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "event_callback",
          event: mockEvent,
        },
      })

      await handleSlackEvents(mockReq, mockRes as Response)

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(consoleLogSpy).toHaveBeenCalledWith(
        expect.stringContaining("Processing Slack event: message"),
      )
      expect(mockDependencies.processReaction).not.toHaveBeenCalled()
    })

    it("should handle requests without event property", async () => {
      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "other_type",
        },
      })

      await handleSlackEvents(mockReq, mockRes as Response)

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockRes.send).toHaveBeenCalledWith("OK")
      expect(mockDependencies.processReaction).not.toHaveBeenCalled()
    })

    it("should throw internalServerError when errors occur during processing", async () => {
      const error = new Error("Processing failed")

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "event_callback",
          event: {
            type: "reaction_added",
            reaction: "test",
          },
        },
      })
      ;(mockDependencies.processReaction as jest.MockedFunction<any>).mockRejectedValue(error)

      await expect(handleSlackEvents(mockReq, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Error processing Slack event"),
      })

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("Error handling Slack event"),
      )
    })

    it("should not send error response if headers already sent", async () => {
      const error = new Error("Processing failed")

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "event_callback",
          event: {
            type: "reaction_added",
            reaction: "test",
          },
        },
      })

      // Mock processReaction to throw an error
      ;(mockDependencies.processReaction as jest.MockedFunction<any>).mockRejectedValue(error)

      // Mock the response to simulate headers already sent after the first response
      ;(mockRes.status as jest.MockedFunction<any>).mockImplementationOnce(() => {
        mockRes.headersSent = true
        return mockRes
      })

      await handleSlackEvents(mockReq, mockRes as Response)

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("Error handling Slack event"),
      )
      expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)

      // The status should be called once (for the initial 200 response)
      expect(mockRes.status).toHaveBeenCalledTimes(1)
      expect(mockRes.status).toHaveBeenCalledWith(200)
    })

    it("should throw internalServerError when errors occur during verification", async () => {
      const error = new Error("Verification failed")
      ;(mockDependencies.crypto.createHmac as jest.MockedFunction<any>).mockImplementation(() => {
        throw error
      })

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {},
      })

      await expect(handleSlackEvents(mockReq, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Error processing Slack event"),
      })

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("Error handling Slack event"),
      )
      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should throw forbidden exception when invalid timestamp format", async () => {
      // Force verification to fail by making timingSafeEqual return false
      ;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(false)

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "v0=test-signature",
          "x-slack-request-timestamp": "invalid-timestamp",
        },
        body: {},
      })

      // Should fail verification and throw forbidden exception
      await expect(handleSlackEvents(mockReq, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Access forbidden"),
      })
      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should handle complex event data with nested objects", async () => {
      const complexEvent = {
        type: "reaction_added",
        reaction: "heavy_check_mark",
        user: "U1234567890",
        item: {
          type: "message",
          channel: "C1234567890",
          ts: "1234567890.123456",
        },
        item_user: "U0987654321",
        event_ts: "1234567890.123457",
      }

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "event_callback",
          team_id: "T1234567890",
          api_app_id: "A1234567890",
          event: complexEvent,
          event_id: "Ev1234567890",
          event_time: 1234567890,
        },
      })
      ;(mockDependencies.processReaction as jest.MockedFunction<any>).mockResolvedValue(true)

      await handleSlackEvents(mockReq, mockRes as Response)

      expect(mockDependencies.processReaction).toHaveBeenCalledWith(complexEvent)
      expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
        distinctId: "system",
        event: "slack-approval-reaction-processed",
        properties: {
          reaction: "heavy_check_mark",
          result: "success",
        },
      })
    })

    it("should handle empty request body", async () => {
      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {},
      })

      await handleSlackEvents(mockReq, mockRes as Response)

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockRes.send).toHaveBeenCalledWith("OK")
    })

    it("should handle undefined request body in verification", async () => {
      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: undefined,
      })

      await handleSlackEvents(mockReq, mockRes as Response)

      // Should handle undefined body in JSON.stringify
      expect(mockRes.status).toHaveBeenCalledWith(200)
    })

    it("should throw internalServerError when error occurs after response sent", async () => {
      const circularError = {}
      ;(circularError as any).self = circularError // Create circular reference

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "event_callback",
          event: {
            type: "reaction_added",
            reaction: "test",
          },
        },
      })
      ;(mockDependencies.processReaction as jest.MockedFunction<any>).mockRejectedValue(
        circularError,
      )

      // Since this is an async error that happens after response is sent (status 200),
      // it throws internalServerError but also sends posthog capture because headers were sent
      await expect(handleSlackEvents(mockReq, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Error processing Slack event"),
      })
    })

    it("should throw internalServerError when processReaction throws synchronously", async () => {
      const error = new Error("Sync error")

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {
          type: "event_callback",
          event: {
            type: "reaction_added",
            reaction: "test",
          },
        },
      })
      ;(mockDependencies.processReaction as jest.MockedFunction<any>).mockImplementation(() => {
        throw error
      })

      await expect(handleSlackEvents(mockReq, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Error processing Slack event"),
      })

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("Error handling Slack event"),
      )
    })

    it("should throw internalServerError when errors occur before sending response", async () => {
      const error = new Error("Early error")

      // Mock verification to fail with an error before any response is sent
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockImplementation(
        () => {
          throw error
        },
      )

      mockReq = createMockRequest({
        headers: {
          "x-slack-signature": "valid",
          "x-slack-request-timestamp": "1000000000",
        },
        body: {},
      })

      await expect(handleSlackEvents(mockReq, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Error processing Slack event"),
      })

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("Error handling Slack event"),
      )
      expect(mockRes.status).not.toHaveBeenCalled()
    })
  })

  describe("edge cases", () => {
    it("should handle missing headers object", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )

      const req = createMockRequest({
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(false)
    })

    it("should handle very large timestamp differences", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )
      ;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(
        Number.MAX_SAFE_INTEGER,
      )

      const req = createMockRequest({
        headers: {
          "x-slack-signature": "v0=test-signature",
          "x-slack-request-timestamp": "0",
        },
        body: {},
      })

      const result = verifySlackRequest(req)

      expect(result).toBe(false)
    })

    it("should handle negative timestamps", () => {
      ;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
        "test-secret",
      )
      ;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(1000000000)

      const req = createMockRequest({
        headers: {
          "x-slack-signature": "v0=test-signature",
          "x-slack-request-timestamp": "-1000",
        },
        body: {},
      })

      const result = verifySlackRequest(req)

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