import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from "@jest/globals"
import { Request, Response } from "express"
let isGitHubAppInstalled: typeof import("../is-github-app-installed").isGitHubAppInstalled
let setIsGitHubAppInstalledDependencies: typeof import("../is-github-app-installed").setIsGitHubAppInstalledDependencies
let resetIsGitHubAppInstalledDependencies: typeof import("../is-github-app-installed").resetIsGitHubAppInstalledDependencies

// Extend Request interface to include userId for testing
interface TestRequest extends Request {
  userId: string
}

describe("isGitHubAppInstalled", () => {
  beforeAll(async () => {
    process.env.KEY_VAULT_NAME = "mocked-keyvault-name"
    const mod = await import("../is-github-app-installed")
    isGitHubAppInstalled = mod.isGitHubAppInstalled
    setIsGitHubAppInstalledDependencies = mod.setIsGitHubAppInstalledDependencies
    resetIsGitHubAppInstalledDependencies = mod.resetIsGitHubAppInstalledDependencies
  })
  let mockReq: Partial<TestRequest>
  let mockRes: Partial<Response>
  let mockDependencies: any

  beforeEach(() => {
    // Setup response mock with proper typing
    mockRes = {
      status: jest.fn().mockReturnThis() as any,
      send: jest.fn() as any,
      json: jest.fn() as any,
    }

    // Setup mock dependencies
    mockDependencies = {
      userNickname: jest.fn(),
      getInstallationOctokitByOwner: jest.fn(),
      isUserCollaborator: jest.fn(),
      githubApp: {},
    }

    setIsGitHubAppInstalledDependencies(mockDependencies)
  })

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

  describe("input validation", () => {
    it("should throw missingRequiredFields when owner is missing", async () => {
      mockReq = {
        query: { repo: "test-repo" },
        userId: "test-user-id",
      }

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Missing required fields"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should throw missingRequiredFields when repo is missing", async () => {
      mockReq = {
        query: { owner: "test-owner" },
        userId: "test-user-id",
      }

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Missing required fields"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should throw missingRequiredFields when both owner and repo are missing", async () => {
      mockReq = {
        query: {},
        userId: "test-user-id",
      }

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Missing required fields"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should throw validationFailure when owner is empty string", async () => {
      mockReq = {
        query: { owner: "  ", repo: "test-repo" },
        userId: "test-user-id",
      }

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Validation Failure"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should throw validationFailure when repo is empty string", async () => {
      mockReq = {
        query: { owner: "test-owner", repo: "  " },
        userId: "test-user-id",
      }

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Validation Failure"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should trim whitespace from owner and repo", async () => {
      mockReq = {
        query: { owner: "  test-owner  ", repo: "  test-repo  " },
        userId: "test-user-id",
      }

      mockDependencies.userNickname.mockResolvedValue("test-nickname")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
      mockDependencies.isUserCollaborator.mockResolvedValue(true)

      await isGitHubAppInstalled(mockReq as Request, mockRes as Response)

      expect(mockDependencies.getInstallationOctokitByOwner).toHaveBeenCalledWith(
        mockDependencies.githubApp,
        "test-owner",
        "test-repo",
        "test-user-id",
      )
    })
  })

  describe("authentication", () => {
    beforeEach(() => {
      mockReq = {
        query: { owner: "test-owner", repo: "test-repo" },
        userId: "test-user-id",
      }
    })

    it("should throw unauthorized when user nickname is null", async () => {
      mockDependencies.userNickname.mockResolvedValue(null)

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Unauthorized"),
      })

      expect(mockDependencies.userNickname).toHaveBeenCalledWith("test-user-id")
      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should throw githubInstallationError when getInstallationOctokitByOwner returns Error", async () => {
      const error = new Error("Installation not found")
      mockDependencies.userNickname.mockResolvedValue("test-nickname")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(error)

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Installation not found"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()
    })
  })

  describe("authorization", () => {
    beforeEach(() => {
      mockReq = {
        query: { owner: "test-owner", repo: "test-repo" },
        userId: "test-user-id",
      }

      mockDependencies.userNickname.mockResolvedValue("test-nickname")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
    })

    it("should throw githubNotCollaborator when user is not a collaborator", async () => {
      mockDependencies.isUserCollaborator.mockResolvedValue(false)

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("not a collaborator"),
      })

      expect(mockDependencies.isUserCollaborator).toHaveBeenCalledWith(
        {},
        "test-owner",
        "test-repo",
        "test-nickname",
      )
      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should return true when user is a collaborator", async () => {
      mockDependencies.isUserCollaborator.mockResolvedValue(true)

      await isGitHubAppInstalled(mockReq as Request, mockRes as Response)

      expect(mockDependencies.isUserCollaborator).toHaveBeenCalledWith(
        {},
        "test-owner",
        "test-repo",
        "test-nickname",
      )
      expect(mockRes.json).toHaveBeenCalledWith(true)
    })
  })

  describe("error handling", () => {
    beforeEach(() => {
      mockReq = {
        query: { owner: "test-owner", repo: "test-repo" },
        userId: "test-user-id",
      }
    })

    it("should throw githubInstallationNotFound when error status is 404", async () => {
      const error = { status: 404, message: "Not found" }
      mockDependencies.userNickname.mockResolvedValue("test-nickname")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
      mockDependencies.isUserCollaborator.mockRejectedValue(error)

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("GitHub installation not found"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()
    })

    it("should throw internalServerError for other errors from isUserCollaborator", async () => {
      const error = new Error("Unexpected error")
      mockDependencies.userNickname.mockResolvedValue("test-nickname")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
      mockDependencies.isUserCollaborator.mockRejectedValue(error)

      const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Error checking GitHub App installation"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()

      consoleErrorSpy.mockRestore()
    })

    it("should throw internalServerError for error from userNickname", async () => {
      const error = new Error("Auth0 error")
      mockDependencies.userNickname.mockRejectedValue(error)

      const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Error checking GitHub App installation"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()

      consoleErrorSpy.mockRestore()
    })

    it("should throw internalServerError for error from getInstallationOctokitByOwner", async () => {
      const error = new Error("GitHub API error")
      mockDependencies.userNickname.mockResolvedValue("test-nickname")
      mockDependencies.getInstallationOctokitByOwner.mockRejectedValue(error)

      const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})

      await expect(isGitHubAppInstalled(mockReq as Request, mockRes as Response)).rejects.toMatchObject({
        message: expect.stringContaining("Error checking GitHub App installation"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()

      consoleErrorSpy.mockRestore()
    })
  })

  describe("successful flow", () => {
    it("should return true for valid installation and authorized user", async () => {
      mockReq = {
        query: { owner: "test-owner", repo: "test-repo" },
        userId: "test-user-id",
      }

      const mockOctokit = { rest: { apps: {} } }

      mockDependencies.userNickname.mockResolvedValue("test-nickname")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
      mockDependencies.isUserCollaborator.mockResolvedValue(true)

      await isGitHubAppInstalled(mockReq as Request, mockRes as Response)

      expect(mockDependencies.userNickname).toHaveBeenCalledWith("test-user-id")
      expect(mockDependencies.getInstallationOctokitByOwner).toHaveBeenCalledWith(
        mockDependencies.githubApp,
        "test-owner",
        "test-repo",
        "test-user-id",
      )
      expect(mockDependencies.isUserCollaborator).toHaveBeenCalledWith(
        mockOctokit,
        "test-owner",
        "test-repo",
        "test-nickname",
      )
      expect(mockRes.json).toHaveBeenCalledWith(true)
    })
  })
})
