import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from "@jest/globals"
import { Request, Response } from "express"
let verifyExistingOptimizations: typeof import("../verify-existing-optimizations").verifyExistingOptimizations
let setVerifyExistingOptimizationsDependencies: typeof import("../verify-existing-optimizations").setVerifyExistingOptimizationsDependencies
let resetVerifyExistingOptimizationsDependencies: typeof import("../verify-existing-optimizations").resetVerifyExistingOptimizationsDependencies

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

describe("verifyExistingOptimizations", () => {
  beforeAll(async () => {
    process.env.KEY_VAULT_NAME = "mocked-keyvault-name"
    const mod = await import("../verify-existing-optimizations")
    verifyExistingOptimizations = mod.verifyExistingOptimizations
    setVerifyExistingOptimizationsDependencies = mod.setVerifyExistingOptimizationsDependencies
    resetVerifyExistingOptimizationsDependencies = mod.resetVerifyExistingOptimizationsDependencies
  })
  let mockReq: Partial<TestRequest>
  let mockRes: Partial<Response>
  let mockDependencies: any
  let mockOctokit: any

  beforeEach(() => {
    // Setup request mock
    mockReq = {
      body: {
        repo_owner: "test-owner",
        repo_name: "test-repo",
        pr_number: 123,
      },
      userId: "test-user-id", // Now properly typed
    }

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

    // Setup mock octokit
    mockOctokit = {
      rest: {
        pulls: {
          get: jest.fn(),
          listReviews: jest.fn(),
          listCommentsForReview: jest.fn(),
          listReviewComments: jest.fn(),
        },
        issues: {
          listComments: jest.fn(),
        },
      },
    }

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

    setVerifyExistingOptimizationsDependencies(mockDependencies)
  })

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

  describe("input validation", () => {
    it("should throw missingRequiredFields for missing required fields", async () => {
      mockReq.body = { repo_owner: "test-owner" } // Missing repo_name and pr_number

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

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

    it("should throw missingRequiredFields for zero PR number", async () => {
      mockReq.body = {
        repo_owner: "test-owner",
        repo_name: "test-repo",
        pr_number: 0, // 0 is falsy and should fail validation
      }

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

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

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

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

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

    it("should throw githubInstallationError when installation octokit fails", async () => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(
        new Error("Installation error"),
      )

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

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

  describe("collaborator verification", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
    })

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

      const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})

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

      expect(mockRes.status).not.toHaveBeenCalled()
      expect(consoleSpy).toHaveBeenCalledWith(
        "test-user is not a collaborator on test-owner/test-repo",
      )

      consoleSpy.mockRestore()
    })

    it("should throw internalServerError when collaborator check fails", async () => {
      const error = new Error("API error")
      mockDependencies.isUserCollaborator.mockRejectedValue(error)

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

      await expect(
        verifyExistingOptimizations(mockReq as Request, mockRes as Response),
      ).rejects.toMatchObject({
        message: expect.stringContaining("Failed to verify collaborator status"),
      })

      expect(mockRes.status).not.toHaveBeenCalled()
      // Fix: console.error concatenates the error as a string
      expect(consoleErrorSpy).toHaveBeenCalledWith(`Error checking collaborator status: ${error}`)

      consoleErrorSpy.mockRestore()
    })
  })

  describe("PR retrieval", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
      mockDependencies.isUserCollaborator.mockResolvedValue(true)
    })

    it("should throw githubPrNotFound when PR is not found", async () => {
      const error = new Error("Not found")
      ;(error as any).status = 404
      mockOctokit.rest.pulls.get.mockRejectedValue(error)

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

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

    it("should throw internalServerError for non-404 PR retrieval errors", async () => {
      const error = new Error("API error")
      ;(error as any).status = 500
      mockOctokit.rest.pulls.get.mockRejectedValue(error)

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

      await expect(
        verifyExistingOptimizations(mockReq as Request, mockRes as Response),
      ).rejects.toMatchObject({
        message: expect.stringContaining("Error verifying existing optimizations"),
      })

      // Fix: console.error concatenates the error as a string
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        `Error in /cfapi/verify-existing-optimizations: ${error}`,
      )
      expect(mockRes.status).not.toHaveBeenCalled()

      consoleErrorSpy.mockRestore()
    })

    it("should successfully retrieve PR data", async () => {
      mockOctokit.rest.pulls.get.mockResolvedValue({
        data: { body: "test body" },
      })
      mockOctokit.rest.issues.listComments.mockResolvedValue({ data: [] })
      mockOctokit.rest.pulls.listReviews.mockResolvedValue({ data: [] })
      mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({ data: [] })
      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockOctokit.rest.pulls.get).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        pull_number: 123,
      })
    })
  })

  describe("comments and reviews retrieval", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
      mockDependencies.isUserCollaborator.mockResolvedValue(true)
      mockOctokit.rest.pulls.get.mockResolvedValue({
        data: { body: "test body" },
      })
    })

    it("should throw internalServerError when PR comments retrieval fails", async () => {
      const error = new Error("Comments API error")
      mockOctokit.rest.issues.listComments.mockRejectedValue(error)

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

      await expect(
        verifyExistingOptimizations(mockReq as Request, mockRes as Response),
      ).rejects.toMatchObject({
        message: expect.stringContaining("Failed to retrieve PR comments"),
      })

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

      consoleErrorSpy.mockRestore()
    })

    it("should throw internalServerError when PR reviews retrieval fails", async () => {
      mockOctokit.rest.issues.listComments.mockResolvedValue({ data: [] })
      const error = new Error("Reviews API error")
      mockOctokit.rest.pulls.listReviews.mockRejectedValue(error)

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

      await expect(
        verifyExistingOptimizations(mockReq as Request, mockRes as Response),
      ).rejects.toMatchObject({
        message: expect.stringContaining("Failed to retrieve PR reviews"),
      })

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

      consoleErrorSpy.mockRestore()
    })

    it("should successfully process reviews and comments", async () => {
      mockOctokit.rest.issues.listComments.mockResolvedValue({
        data: [{ body: "Regular comment" }],
      })
      mockOctokit.rest.pulls.listReviews.mockResolvedValue({
        data: [
          { id: 1, body: "Review body 1" },
          { id: 2, body: "Review body 2" },
        ],
      })
      mockOctokit.rest.pulls.listCommentsForReview.mockResolvedValue({
        data: [{ body: "Review comment" }],
      })
      mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({
        data: [{ body: "Additional review comment" }],
      })

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
        function1: new Set(["optimization1"]),
      })

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      // Verify that all comments types are collected
      expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
        "test body",
        expect.arrayContaining([
          { body: "Regular comment" },
          { body: "Review body 1" },
          { body: "Review body 2" },
          { body: "Review comment" },
          { body: "Additional review comment" },
        ]),
      )
    })

    it("should filter out comments with undefined body", async () => {
      mockOctokit.rest.issues.listComments.mockResolvedValue({
        data: [{ body: "Valid comment" }, { body: undefined }, { body: "Another valid comment" }],
      })
      mockOctokit.rest.pulls.listReviews.mockResolvedValue({ data: [] })
      mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({ data: [] })

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
        function1: new Set(["optimization1"]),
      })

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith("test body", [
        { body: "Valid comment" },
        { body: "Another valid comment" },
      ])
    })
  })

  describe("optimizations processing", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
      mockDependencies.isUserCollaborator.mockResolvedValue(true)
      mockOctokit.rest.pulls.get.mockResolvedValue({
        data: { body: "test body" },
      })
      mockOctokit.rest.issues.listComments.mockResolvedValue({
        data: [{ body: "test comment" }],
      })
      mockOctokit.rest.pulls.listReviews.mockResolvedValue({ data: [] })
      mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({ data: [] })
    })

    it("should return 404 when no optimizations are found", async () => {
      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "No optimizations found for this PR",
      })
    })

    it("should handle null PR body", async () => {
      mockOctokit.rest.pulls.get.mockResolvedValue({
        data: { body: null },
      })
      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
        "", // null body should become empty string
        [{ body: "test comment" }],
      )
    })

    it("should handle undefined PR body", async () => {
      mockOctokit.rest.pulls.get.mockResolvedValue({
        data: { body: undefined },
      })
      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
        "", // undefined body should become empty string
        [{ body: "test comment" }],
      )
    })

    it("should convert Set to array format and return optimizations", async () => {
      const mockOptimizationsDict = {
        function1: new Set(["optimization1", "optimization2"]),
        function2: new Set(["optimization3"]),
      }

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue(mockOptimizationsDict)

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      const expectedResponse = {
        function1: ["optimization1", "optimization2"],
        function2: ["optimization3"],
      }

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
    })

    it("should track analytics event", async () => {
      const mockOptimizationsDict = {
        function1: new Set(["optimization1"]),
      }

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue(mockOptimizationsDict)

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
        distinctId: "test-user-id",
        event: "cfapi-github-pr-optimization",
        properties: {
          repo_owner: "test-owner",
          repo_name: "test-repo",
          pr_number: 123,
        },
      })
    })

    it("should handle empty sets in optimizations dict", async () => {
      const mockOptimizationsDict = {
        function1: new Set([]),
        function2: new Set(["optimization1"]),
      }

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue(mockOptimizationsDict)

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      const expectedResponse = {
        function1: [],
        function2: ["optimization1"],
      }

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
    })
  })

  describe("error handling", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
      mockDependencies.isUserCollaborator.mockResolvedValue(true)
    })

    it("should throw internalServerError for general errors with Error objects", async () => {
      const error = new Error("Unexpected error")
      mockOctokit.rest.pulls.get.mockRejectedValue(error)

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

      await expect(
        verifyExistingOptimizations(mockReq as Request, mockRes as Response),
      ).rejects.toMatchObject({
        message: expect.stringContaining("Error verifying existing optimizations"),
      })

      // Fix: console.error concatenates the error as a string
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        `Error in /cfapi/verify-existing-optimizations: ${error}`,
      )
      expect(mockRes.status).not.toHaveBeenCalled()

      consoleErrorSpy.mockRestore()
    })

    it("should throw internalServerError for non-Error exceptions", async () => {
      mockOctokit.rest.pulls.get.mockRejectedValue("String error")

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

      await expect(
        verifyExistingOptimizations(mockReq as Request, mockRes as Response),
      ).rejects.toMatchObject({
        message: expect.stringContaining("Error verifying existing optimizations"),
      })

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

      consoleErrorSpy.mockRestore()
    })
  })

  describe("edge cases", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
      mockDependencies.isUserCollaborator.mockResolvedValue(true)
      mockOctokit.rest.pulls.get.mockResolvedValue({
        data: { body: "test body" },
      })
      mockOctokit.rest.issues.listComments.mockResolvedValue({ data: [] })
      mockOctokit.rest.pulls.listReviews.mockResolvedValue({ data: [] })
      mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({ data: [] })
    })

    it("should handle review comment retrieval failures gracefully", async () => {
      mockOctokit.rest.pulls.listReviews.mockResolvedValue({
        data: [{ id: 1, body: "Review body" }],
      })
      mockOctokit.rest.pulls.listCommentsForReview.mockRejectedValue(new Error("Review API error"))

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

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
        function1: new Set(["optimization1"]),
      })

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        "Error getting review comments for review 1:",
        expect.objectContaining({
          message: "Review API error",
          name: "Error",
        }),
      )
      // Should still continue and succeed
      expect(mockRes.status).toHaveBeenCalledWith(200)

      consoleErrorSpy.mockRestore()
    })

    it("should handle all review comments retrieval failure gracefully", async () => {
      mockOctokit.rest.pulls.listReviewComments.mockRejectedValue(
        new Error("All review comments API error"),
      )

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

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
        function1: new Set(["optimization1"]),
      })

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        "Error getting all review comments:",
        expect.objectContaining({
          message: "All review comments API error",
          name: "Error",
        }),
      )
      // Should still continue and succeed
      expect(mockRes.status).toHaveBeenCalledWith(200)

      consoleErrorSpy.mockRestore()
    })

    it("should handle valid non-zero PR number", async () => {
      mockReq.body.pr_number = 999
      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockOctokit.rest.pulls.get).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        pull_number: 999,
      })
    })

    it("should handle very large optimizations dict", async () => {
      const largeOptimizationsDict: { [key: string]: Set<string> } = {}

      // Create 100 functions with 50 optimizations each
      for (let i = 0; i < 100; i++) {
        const functionName = `function${i}`
        const optimizations = new Set<string>()
        for (let j = 0; j < 50; j++) {
          optimizations.add(`optimization${i}_${j}`)
        }
        largeOptimizationsDict[functionName] = optimizations
      }

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue(largeOptimizationsDict)

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockRes.status).toHaveBeenCalledWith(200)

      // Verify the response structure is correct
      const jsonCall = (mockRes.json as jest.MockedFunction<any>).mock.calls[0][0]
      expect(Object.keys(jsonCall)).toHaveLength(100)
      expect(jsonCall.function0).toHaveLength(50)
      expect(jsonCall.function99).toHaveLength(50)
    })

    it("should handle reviews with no body", async () => {
      mockOctokit.rest.pulls.listReviews.mockResolvedValue({
        data: [
          { id: 1, body: "Review with body" },
          { id: 2, body: null },
          { id: 3, body: "" },
          { id: 4 }, // no body property
        ],
      })
      mockOctokit.rest.pulls.listCommentsForReview.mockResolvedValue({ data: [] })

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
        function1: new Set(["optimization1"]),
      })

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      // Should only include reviews with truthy body values
      expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
        "test body",
        expect.arrayContaining([{ body: "Review with body" }]),
      )
    })

    it("should handle complex review structure", async () => {
      mockOctokit.rest.pulls.listReviews.mockResolvedValue({
        data: [{ id: 1, body: "Main review body" }],
      })
      mockOctokit.rest.pulls.listCommentsForReview.mockResolvedValue({
        data: [
          { body: "Review comment 1" },
          { body: "Review comment 2" },
          { body: null }, // Should be filtered out
        ],
      })
      mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({
        data: [{ body: "General review comment 1" }, { body: "General review comment 2" }],
      })

      mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
        function1: new Set(["optimization1"]),
      })

      await verifyExistingOptimizations(mockReq as Request, mockRes as Response)

      expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
        "test body",
        expect.arrayContaining([
          { body: "Main review body" },
          { body: "Review comment 1" },
          { body: "Review comment 2" },
          { body: "General review comment 1" },
          { body: "General review comment 2" },
        ]),
      )
    })
  })
})
