import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import {
  sendSlackMessage,
  setSendSlackMessageDependencies,
  resetSendSlackMessageDependencies,
  initializeWebClient,
  resetWebClient,
} from "../slack_util"

describe("slack-message", () => {
  let mockDependencies: any
  let mockWebClient: any

  beforeEach(() => {
    // Reset web client before each test
    resetWebClient()

    // Setup mock web client
    mockWebClient = {
      chat: {
        postMessage: jest.fn(),
      },
    }

    // Setup mock dependencies
    mockDependencies = {
      WebClient: jest.fn().mockReturnValue(mockWebClient),
      getSlackToken: jest.fn(),
      getSlackChannelId: jest.fn(),
      console: {
        log: jest.fn(),
        warn: jest.fn(),
        error: jest.fn(),
      },
    }

    setSendSlackMessageDependencies(mockDependencies)
  })

  afterEach(() => {
    resetSendSlackMessageDependencies()
    resetWebClient()
    jest.clearAllMocks()
  })

  describe("initializeWebClient", () => {
    it("should throw error when SLACK_TOKEN is missing", () => {
      mockDependencies.getSlackToken.mockReturnValue(undefined)
      mockDependencies.getSlackChannelId.mockReturnValue("channel-id")

      expect(() => initializeWebClient()).toThrow("Missing SLACK_TOKEN")
    })

    it("should throw error when SLACK_CHANNEL_ID is missing", () => {
      mockDependencies.getSlackToken.mockReturnValue("token")
      mockDependencies.getSlackChannelId.mockReturnValue(undefined)

      expect(() => initializeWebClient()).toThrow("Missing SLACK_CHANNEL_ID")
    })

    it("should create WebClient with token when both env vars are present", () => {
      mockDependencies.getSlackToken.mockReturnValue("test-token")
      mockDependencies.getSlackChannelId.mockReturnValue("test-channel")

      const client = initializeWebClient()

      expect(mockDependencies.WebClient).toHaveBeenCalledWith("test-token", {})
      expect(client).toBe(mockWebClient)
    })

    it("should reuse existing WebClient on subsequent calls", () => {
      mockDependencies.getSlackToken.mockReturnValue("test-token")
      mockDependencies.getSlackChannelId.mockReturnValue("test-channel")

      const client1 = initializeWebClient()
      const client2 = initializeWebClient()

      expect(mockDependencies.WebClient).toHaveBeenCalledTimes(1)
      expect(client1).toBe(client2)
    })

    it("should throw error when SLACK_TOKEN is empty string", () => {
      mockDependencies.getSlackToken.mockReturnValue("")
      mockDependencies.getSlackChannelId.mockReturnValue("channel-id")

      expect(() => initializeWebClient()).toThrow("Missing SLACK_TOKEN")
    })

    it("should throw error when SLACK_CHANNEL_ID is empty string", () => {
      mockDependencies.getSlackToken.mockReturnValue("token")
      mockDependencies.getSlackChannelId.mockReturnValue("")

      expect(() => initializeWebClient()).toThrow("Missing SLACK_CHANNEL_ID")
    })

    it("should throw error when SLACK_TOKEN is null", () => {
      mockDependencies.getSlackToken.mockReturnValue(null)
      mockDependencies.getSlackChannelId.mockReturnValue("channel-id")

      expect(() => initializeWebClient()).toThrow("Missing SLACK_TOKEN")
    })

    it("should throw error when SLACK_CHANNEL_ID is null", () => {
      mockDependencies.getSlackToken.mockReturnValue("token")
      mockDependencies.getSlackChannelId.mockReturnValue(null)

      expect(() => initializeWebClient()).toThrow("Missing SLACK_CHANNEL_ID")
    })
  })

  describe("sendSlackMessage", () => {
    beforeEach(() => {
      // Setup default successful environment
      mockDependencies.getSlackToken.mockReturnValue("test-token")
      mockDependencies.getSlackChannelId.mockReturnValue("test-channel")
    })

    it("should send string message successfully", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage("Hello World")

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Hello World",
      })
      expect(result).toBe(true)
    })

    it("should send string message with custom channel", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage("Hello World", "custom-channel")

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "custom-channel",
        text: "Hello World",
      })
      expect(result).toBe(true)
    })

    it("should send object with blocks successfully", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const message = {
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "Hello World",
            },
          },
        ],
      }

      const result = await sendSlackMessage(message)

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Notification from CodeFlash",
        blocks: message.blocks,
      })
      expect(result).toBe(true)
    })

    it("should use custom text when provided with blocks", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const message = {
        text: "Custom fallback text",
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "Hello World",
            },
          },
        ],
      }

      const result = await sendSlackMessage(message)

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Custom fallback text",
        blocks: message.blocks,
      })
      expect(result).toBe(true)
    })

    it("should handle object without blocks property", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const message = { key: "value", nested: { prop: "test" } }

      const result = await sendSlackMessage(message)

      expect(mockDependencies.console.warn).toHaveBeenCalledWith(
        "Object passed to sendSlackMessage without blocks property",
      )
      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: JSON.stringify(message),
      })
      expect(result).toBe(true)
    })

    it("should handle invalid message types", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage(123)

      expect(mockDependencies.console.error).toHaveBeenCalledWith("Invalid message type", "number")
      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Invalid message",
      })
      expect(result).toBe(true)
    })

    it("should handle null message", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage(null)

      expect(mockDependencies.console.error).toHaveBeenCalledWith("Invalid message type", "object")
      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Invalid message",
      })
      expect(result).toBe(true)
    })

    it("should handle undefined message", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage(undefined)

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Invalid message type",
        "undefined",
      )
      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Invalid message",
      })
      expect(result).toBe(true)
    })

    it("should return full response when returnData is true", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456", channel: "C123" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage("Hello World", null, true)

      expect(result).toEqual(mockResponse)
    })

    it("should handle errors and still resolve", async () => {
      const error = new Error("Slack API error")
      mockWebClient.chat.postMessage.mockRejectedValue(error)

      const result = await sendSlackMessage("Hello World")

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error sending Slack message:",
        error,
      )
      expect(result).toBe(true)
    })

    it("should return error object when returnData is true and error occurs", async () => {
      const error = new Error("Slack API error")
      mockWebClient.chat.postMessage.mockRejectedValue(error)

      const result = await sendSlackMessage("Hello World", null, true)

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error sending Slack message:",
        error,
      )
      expect(result).toEqual({ error })
    })

    it("should handle empty string message", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage("")

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "",
      })
      expect(result).toBe(true)
    })

    it("should handle boolean message type", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage(true)

      expect(mockDependencies.console.error).toHaveBeenCalledWith("Invalid message type", "boolean")
      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Invalid message",
      })
      expect(result).toBe(true)
    })

    it("should handle array message type", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const result = await sendSlackMessage([1, 2, 3])

      // Arrays are objects, so they go through the object path and trigger a warning
      expect(mockDependencies.console.warn).toHaveBeenCalledWith(
        "Object passed to sendSlackMessage without blocks property",
      )
      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "[1,2,3]",
      })
      expect(result).toBe(true)
    })

    it("should handle complex nested blocks", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const message = {
        text: "Fallback",
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "Hello World",
            },
            accessory: {
              type: "button",
              text: {
                type: "plain_text",
                text: "Click Me",
              },
              action_id: "button_1",
            },
          },
          {
            type: "divider",
          },
          {
            type: "context",
            elements: [
              {
                type: "mrkdwn",
                text: "Context text",
              },
            ],
          },
        ],
      }

      const result = await sendSlackMessage(message)

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Fallback",
        blocks: message.blocks,
      })
      expect(result).toBe(true)
    })

    it("should handle object with empty blocks array", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const message = {
        blocks: [],
      }

      const result = await sendSlackMessage(message)

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Notification from CodeFlash",
        blocks: [],
      })
      expect(result).toBe(true)
    })

    it("should handle circular reference in object without blocks", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const message: any = { key: "value" }
      message.circular = message // Create circular reference

      // Save the original JSON.stringify
      const originalStringify = JSON.stringify

      // Replace JSON.stringify with a properly typed mock
      global.JSON.stringify = ((obj: any): string => {
        if (obj === message) {
          throw new TypeError("Converting circular structure to JSON")
        }
        return originalStringify(obj)
      }) as any

      try {
        await sendSlackMessage(message)

        expect(mockDependencies.console.warn).toHaveBeenCalledWith(
          "Object passed to sendSlackMessage without blocks property",
        )
        expect(mockDependencies.console.error).toHaveBeenCalledWith(
          "Error sending Slack message:",
          expect.any(TypeError),
        )
      } finally {
        // Always restore JSON.stringify
        global.JSON.stringify = originalStringify
      }
    })

    it("should handle initialization error", async () => {
      mockDependencies.getSlackToken.mockReturnValue(undefined)

      const result = await sendSlackMessage("Hello World")

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error sending Slack message:",
        expect.any(Error),
      )
      expect(result).toBe(true)
    })

    it("should handle WebClient constructor error", async () => {
      const error = new Error("WebClient initialization failed")
      mockDependencies.WebClient.mockImplementation(() => {
        throw error
      })

      const result = await sendSlackMessage("Hello World", null, true)

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error sending Slack message:",
        error,
      )
      expect(result).toEqual({ error })
    })

    it("should use channel parameter over environment variable", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      await sendSlackMessage("Test", "override-channel")

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "override-channel",
        text: "Test",
      })
    })

    it("should handle empty channel parameter by using default", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      await sendSlackMessage("Test", "")

      // Empty string is falsy, so it should fall back to the default channel
      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: "Test",
      })
    })

    it("should handle very long messages", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const longMessage = "x".repeat(10000)
      const result = await sendSlackMessage(longMessage)

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: longMessage,
      })
      expect(result).toBe(true)
    })

    it("should handle special characters in message", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)

      const specialMessage = "Hello <@U123> & team! Check this: https://example.com"
      const result = await sendSlackMessage(specialMessage)

      expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
        channel: "test-channel",
        text: specialMessage,
      })
      expect(result).toBe(true)
    })

    it("should handle promise rejection from WebClient", async () => {
      mockWebClient.chat.postMessage.mockRejectedValue("Network error")

      const result = await sendSlackMessage("Test", null, true)

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error sending Slack message:",
        "Network error",
      )
      expect(result).toEqual({ error: "Network error" })
    })

    it("should handle synchronous error in postMessage", async () => {
      mockWebClient.chat.postMessage.mockImplementation(() => {
        throw new Error("Sync error")
      })

      const result = await sendSlackMessage("Test")

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error sending Slack message:",
        expect.any(Error),
      )
      expect(result).toBe(true)
    })
  })

  describe("edge cases", () => {
    it("should handle getSlackToken throwing error", async () => {
      mockDependencies.getSlackToken.mockImplementation(() => {
        throw new Error("Env error")
      })
      mockDependencies.getSlackChannelId.mockReturnValue("channel")

      const result = await sendSlackMessage("Test")

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error sending Slack message:",
        expect.any(Error),
      )
      expect(result).toBe(true)
    })

    it("should handle getSlackChannelId throwing error", async () => {
      mockDependencies.getSlackToken.mockReturnValue("token")
      mockDependencies.getSlackChannelId.mockImplementation(() => {
        throw new Error("Env error")
      })

      const result = await sendSlackMessage("Test")

      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error sending Slack message:",
        expect.any(Error),
      )
      expect(result).toBe(true)
    })

    it("should handle object with null blocks property", async () => {
      const mockResponse = { ok: true, ts: "1234567890.123456" }
      mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
      mockDependencies.getSlackToken.mockReturnValue("token")
      mockDependencies.getSlackChannelId.mockReturnValue("channel")

      const message = { blocks: null }

      const result = await sendSlackMessage(message)

      expect(mockDependencies.console.warn).toHaveBeenCalledWith(
        "Object passed to sendSlackMessage without blocks property",
      )
      expect(result).toBe(true)
    })
  })
})
