import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"

// Mock Sentry before importing modules that use it
const mockCaptureException = jest.fn()
jest.unstable_mockModule("@sentry/node", () => ({
  captureException: mockCaptureException,
  captureMessage: jest.fn(),
  addBreadcrumb: jest.fn(),
}))

// Dynamic import after mock setup
const {
  ensureLabelExists,
  addLabelToPullRequest,
  isUserCollaborator,
  getInstallationOctokitByOwner,
  setGithubUtilsDependencies,
  resetGithubUtilsDependencies,
} = await import("../github-utils.js")

describe("GitHub Utils", () => {
  let mockOctokit: any
  let mockGithubApp: any
  let mockDependencies: any

  beforeEach(() => {
    // Setup mock Octokit
    mockOctokit = {
      rest: {
        repos: {
          checkCollaborator: jest.fn(),
        },
        issues: {
          getLabel: jest.fn(),
          createLabel: jest.fn(),
          addLabels: jest.fn(),
        },
      },
    }

    // Setup mock GitHub App
    mockGithubApp = {
      octokit: {
        rest: {
          apps: {
            getRepoInstallation: jest.fn(),
          },
        },
      },
      getInstallationOctokit: jest.fn(),
    }

    // Setup mock dependencies
    mockDependencies = {
      console: {
        error: jest.fn(),
      },
    }

    setGithubUtilsDependencies(mockDependencies)
  })

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

  describe("isUserCollaborator", () => {
    it("should return true when user is a collaborator (status 204)", async () => {
      ;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockResolvedValue({
        status: 204,
      })

      const result = await isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user")

      expect(result).toBe(true)
      expect(mockOctokit.rest.repos.checkCollaborator).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        username: "test-user",
      })
    })

    it("should return false when user is a collaborator but status is not 204", async () => {
      ;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockResolvedValue({
        status: 200,
      })

      const result = await isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user")

      expect(result).toBe(false)
    })

    it("should return false when user is not a collaborator (404 error)", async () => {
      const error = new Error("Not Found")
      ;(error as any).status = 404
      ;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockRejectedValue(
        error,
      )

      const result = await isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user")

      expect(result).toBe(false)
    })

    it("should throw error for non-404 API errors", async () => {
      const error = new Error("Internal Server Error")
      ;(error as any).status = 500
      ;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockRejectedValue(
        error,
      )

      await expect(
        isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user"),
      ).rejects.toThrow("Internal Server Error")
    })

    it("should throw error for API errors without status", async () => {
      const error = new Error("Network Error")
      ;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockRejectedValue(
        error,
      )

      await expect(
        isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user"),
      ).rejects.toThrow("Network Error")
    })

    it("should handle empty username", async () => {
      ;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockResolvedValue({
        status: 204,
      })

      const result = await isUserCollaborator(mockOctokit, "test-owner", "test-repo", "")

      expect(result).toBe(true)
      expect(mockOctokit.rest.repos.checkCollaborator).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        username: "",
      })
    })

    it("should handle special characters in username", async () => {
      ;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockResolvedValue({
        status: 204,
      })

      const result = await isUserCollaborator(
        mockOctokit,
        "test-owner",
        "test-repo",
        "user-with-dashes_and_underscores",
      )

      expect(result).toBe(true)
      expect(mockOctokit.rest.repos.checkCollaborator).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        username: "user-with-dashes_and_underscores",
      })
    })
  })

  describe("getInstallationOctokitByOwner", () => {
    it("should return Octokit instance when installation exists", async () => {
      const mockInstallationOctokit = {
        rest: {
          /* mock octokit */
        },
      }

      ;(
        mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
      ).mockResolvedValue({ data: { id: 12345 } })
      ;(mockGithubApp.getInstallationOctokit as jest.MockedFunction<any>).mockResolvedValue(
        mockInstallationOctokit,
      )

      const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")

      expect(result).toBe(mockInstallationOctokit)
      expect(mockGithubApp.octokit.rest.apps.getRepoInstallation).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
      })
      expect(mockGithubApp.getInstallationOctokit).toHaveBeenCalledWith(12345)
    })

    it("should return Error when GitHub App is not installed (404)", async () => {
      const error = new Error("Not Found")
      ;(error as any).status = 404
      ;(
        mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
      ).mockRejectedValue(error)

      const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")

      expect(result).toBeInstanceOf(Error)
      expect((result as Error).message).toBe(
        "GitHub App is not installed on the repository test-owner/test-repo",
      )
      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error getting installation Octokit for test-owner/test-repo:",
        error,
      )
    })

    it("should return Error for other API errors", async () => {
      const error = new Error("Internal Server Error")
      ;(error as any).status = 500
      ;(
        mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
      ).mockRejectedValue(error)

      const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")

      expect(result).toBeInstanceOf(Error)
      expect((result as Error).message).toBe(
        "Error checking GitHub App installation status for repo test-owner/test-repo",
      )
      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error getting installation Octokit for test-owner/test-repo:",
        error,
      )
    })

    it("should return Error when getInstallationOctokit fails", async () => {
      ;(
        mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
      ).mockResolvedValue({ data: { id: 12345 } })

      const installationError = new Error("Installation Octokit failed")
      ;(mockGithubApp.getInstallationOctokit as jest.MockedFunction<any>).mockRejectedValue(
        installationError,
      )

      const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")

      expect(result).toBeInstanceOf(Error)
      expect((result as Error).message).toBe(
        "Error checking GitHub App installation status for repo test-owner/test-repo",
      )
      expect(mockDependencies.console.error).toHaveBeenCalledWith(
        "Error getting installation Octokit for test-owner/test-repo:",
        installationError,
      )
    })

    it("should handle missing installation ID", async () => {
      ;(
        mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
      ).mockResolvedValue({ data: {} }) // Missing id

      const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")

      expect(mockGithubApp.getInstallationOctokit).toHaveBeenCalledWith(undefined)
      // The behavior depends on what getInstallationOctokit does with undefined
    })

    it("should handle network errors without status", async () => {
      const error = new Error("Network timeout")
      ;(
        mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
      ).mockRejectedValue(error)

      const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")

      expect(result).toBeInstanceOf(Error)
      expect((result as Error).message).toBe(
        "Error checking GitHub App installation status for repo test-owner/test-repo",
      )
    })
  })

  describe("ensureLabelExists", () => {
    it("should not create label when it already exists", async () => {
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
        data: { name: "test-label" },
      })

      await ensureLabelExists(
        mockOctokit,
        "test-owner",
        "test-repo",
        "test-label",
        "FF0000",
        "Test label description",
      )

      expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        name: "test-label",
      })
      expect(mockOctokit.rest.issues.createLabel).not.toHaveBeenCalled()
    })

    it("should create label when it doesn't exist (404 error)", async () => {
      const error = new Error("Not Found")
      ;(error as any).status = 404
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)

      await ensureLabelExists(
        mockOctokit,
        "test-owner",
        "test-repo",
        "test-label",
        "FF0000",
        "Test label description",
      )

      expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        name: "test-label",
      })
      expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        name: "test-label",
        color: "FF0000",
        description: "Test label description",
      })
    })

    it("should log to Sentry on label creation failure instead of throwing", async () => {
      const getError = new Error("Not Found")
      ;(getError as any).status = 404
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(getError)

      const createError = new Error("Label creation failed")
      ;(mockOctokit.rest.issues.createLabel as jest.MockedFunction<any>).mockRejectedValue(
        createError,
      )

      await ensureLabelExists(
        mockOctokit,
        "test-owner",
        "test-repo",
        "test-label",
        "FF0000",
        "Test label description",
      )

      expect(mockCaptureException).toHaveBeenCalledWith(createError, {
        extra: {
          owner: "test-owner",
          repo: "test-repo",
          labelName: "test-label",
          context: "ensureLabelExists - createLabel failed",
        },
      })
    })

    it("should not log to Sentry when label already exists (race condition)", async () => {
      const getError = new Error("Not Found")
      ;(getError as any).status = 404
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(getError)

      const alreadyExistsError = new Error("Validation Failed")
      ;(alreadyExistsError as any).status = 422
      ;(alreadyExistsError as any).response = {
        data: {
          errors: [{ resource: "Label", code: "already_exists", field: "name" }],
        },
      }
      ;(mockOctokit.rest.issues.createLabel as jest.MockedFunction<any>).mockRejectedValue(
        alreadyExistsError,
      )

      await ensureLabelExists(
        mockOctokit,
        "test-owner",
        "test-repo",
        "test-label",
        "FF0000",
        "Test label description",
      )

      expect(mockCaptureException).not.toHaveBeenCalled()
    })

    it("should handle special characters in label name", async () => {
      const error = new Error("Not Found")
      ;(error as any).status = 404
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)

      await ensureLabelExists(
        mockOctokit,
        "test-owner",
        "test-repo",
        "⚡️ special-label",
        "FFC043",
        "Label with emoji and special chars",
      )

      expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        name: "⚡️ special-label",
        color: "FFC043",
        description: "Label with emoji and special chars",
      })
    })

    it("should handle empty color and description", async () => {
      const error = new Error("Not Found")
      ;(error as any).status = 404
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)

      await ensureLabelExists(mockOctokit, "test-owner", "test-repo", "test-label", "", "")

      expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        name: "test-label",
        color: "",
        description: "",
      })
    })
  })

  describe("addLabelToPullRequest", () => {
    beforeEach(() => {
      // Mock ensureLabelExists to always succeed
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
        data: { name: "⚡️ codeflash" },
      })
    })

    it("should add label to pull request with default parameters", async () => {
      await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 123)

      expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        name: "⚡️ codeflash",
      })
      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        issue_number: 123,
        labels: ["⚡️ codeflash"],
      })
    })

    it("should add custom label to pull request", async () => {
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
        data: { name: "custom-label" },
      })

      await addLabelToPullRequest(
        mockOctokit,
        "test-owner",
        "test-repo",
        456,
        "custom-label",
        "00FF00",
        "Custom label description",
      )

      expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        name: "custom-label",
      })
      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        issue_number: 456,
        labels: ["custom-label"],
      })
    })

    it("should create label if it doesn't exist and then add it", async () => {
      const error = new Error("Not Found")
      ;(error as any).status = 404
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)

      await addLabelToPullRequest(
        mockOctokit,
        "test-owner",
        "test-repo",
        789,
        "new-label",
        "0000FF",
        "New label description",
      )

      expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        name: "new-label",
        color: "0000FF",
        description: "New label description",
      })
      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        issue_number: 789,
        labels: ["new-label"],
      })
    })

    it("should handle addLabels API failure", async () => {
      const error = new Error("Failed to add labels")
      ;(mockOctokit.rest.issues.addLabels as jest.MockedFunction<any>).mockRejectedValue(error)

      await expect(
        addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 123),
      ).rejects.toThrow("Failed to add labels")
    })

    it("should continue to add labels even when ensureLabelExists logs error to Sentry", async () => {
      const error = new Error("Label operation failed")
      ;(error as any).status = 500
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)

      await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 123)

      expect(mockCaptureException).toHaveBeenCalledWith(error, {
        extra: {
          owner: "test-owner",
          repo: "test-repo",
          labelName: "⚡️ codeflash",
          context: "ensureLabelExists - getLabel failed",
        },
      })
      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        issue_number: 123,
        labels: ["⚡️ codeflash"],
      })
    })

    it("should handle zero pull request number", async () => {
      await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 0)

      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        issue_number: 0,
        labels: ["⚡️ codeflash"],
      })
    })

    it("should handle negative pull request number", async () => {
      await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", -1)

      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        issue_number: -1,
        labels: ["⚡️ codeflash"],
      })
    })

    it("should handle very long label name", async () => {
      const longLabelName = "a".repeat(100) // Very long label name
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
        data: { name: longLabelName },
      })

      await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 123, longLabelName)

      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        issue_number: 123,
        labels: [longLabelName],
      })
    })
  })

  describe("integration scenarios", () => {
    it("should handle complete workflow for new repository", async () => {
      // Simulate adding label to a PR in a new repository where the label doesn't exist
      const getError = new Error("Not Found")
      ;(getError as any).status = 404
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(getError)

      await addLabelToPullRequest(
        mockOctokit,
        "new-owner",
        "new-repo",
        1,
        "🚀 optimization",
        "FF6B6B",
        "Performance optimization",
      )

      // Verify label creation
      expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
        owner: "new-owner",
        repo: "new-repo",
        name: "🚀 optimization",
        color: "FF6B6B",
        description: "Performance optimization",
      })

      // Verify label addition
      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "new-owner",
        repo: "new-repo",
        issue_number: 1,
        labels: ["🚀 optimization"],
      })
    })

    it("should handle repository with existing labels", async () => {
      // Simulate adding label to a PR where the label already exists
      ;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
        data: { name: "existing-label" },
      })

      await addLabelToPullRequest(
        mockOctokit,
        "existing-owner",
        "existing-repo",
        42,
        "existing-label",
      )

      // Verify no label creation
      expect(mockOctokit.rest.issues.createLabel).not.toHaveBeenCalled()

      // Verify label addition
      expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
        owner: "existing-owner",
        repo: "existing-repo",
        issue_number: 42,
        labels: ["existing-label"],
      })
    })
  })
})
