import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from "@jest/globals"
import { Request, Response } from "express"
import {
  suggestPrChanges,
  triggerSuggestPrChanges,
  setSuggestPrChangesDependencies,
  resetSuggestPrChangesDependencies,
} from "../suggest-pr-changes"
// Logger is mocked globally in jest.setup.ts - no need to mock here
// The logger calls go through to console which we can spy on

// Mock types for testing
interface AuthorizedUserReq extends Request {
  userId: string
}

// Note: Global mocks are set up in jest.setup.ts

describe("Suggest PR Changes", () => {
  beforeAll(() => {
    process.env.KEY_VAULT_NAME = "mocked-keyvault-name"
  })
  let mockReq: Partial<AuthorizedUserReq>
  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 comprehensive mock dependencies
    mockDependencies = {
      prisma: {
        optimization_features: {
          findUnique: jest.fn(),
          update: jest.fn(),
        },
        optimization_events: {
          update: jest.fn(),
        },
      },
      userNickname: jest.fn(),
      getInstallationOctokitByOwner: jest.fn(),
      isUserCollaborator: jest.fn(),
      requiresApproval: jest.fn(),
      requestApproval: jest.fn(),
      posthog: {
        capture: jest.fn(),
      },
      githubApp: {},
      isDiffContentsWellFormed: jest.fn(),
      fileDiffsToMap: jest.fn(),
      determineValidHunks: jest.fn(),
      buildDependentPrTitle: jest.fn(),
      buildPrCommentBody: jest.fn(),
      createNewBranchFromDiffContents: jest.fn(),
      createDependentPullRequest: jest.fn(),
      sendSlackMessage: jest.fn(),
      updateOptimizationEvent: jest.fn(),
      saveStagingReview: jest.fn(),
    }

    setSuggestPrChangesDependencies(mockDependencies)
  })

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

  describe("suggestPrChanges", () => {
    describe("input validation", () => {
      it("should return 400 for missing required fields", async () => {
        mockReq = {
          body: {
            owner: "test-owner",
            // missing repo, pullNumber, diffContents
          },
          userId: "test-user-id",
        } as AuthorizedUserReq

        await expect(
          suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response),
        ).rejects.toMatchObject({
          message: expect.stringContaining("Missing or malformed fields"),
        })
        expect(mockRes.status).not.toHaveBeenCalled()
      })

      it("should return 400 for malformed diff contents", async () => {
        mockReq = {
          body: {
            owner: "test-owner",
            repo: "test-repo",
            pullNumber: 1,
            diffContents: "invalid-diff",
          },
          userId: "test-user-id",
        } as AuthorizedUserReq
        mockDependencies.isDiffContentsWellFormed.mockReturnValue(false)

        await expect(
          suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response),
        ).rejects.toMatchObject({
          message: expect.stringContaining("Missing or malformed fields"),
        })
        expect(mockRes.status).not.toHaveBeenCalled()
      })
    })

    describe("authentication and authorization", () => {
      beforeEach(() => {
        mockReq = {
          body: {
            owner: "test-owner",
            repo: "test-repo",
            pullNumber: 1,
            diffContents: [{ file: "test.js", content: "test" }],
            prCommentFields: { function_name: "testFunc" },
          },
          userId: "test-user-id",
        } as AuthorizedUserReq
        mockDependencies.isDiffContentsWellFormed.mockReturnValue(true)
      })

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

        await expect(
          suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response),
        ).rejects.toMatchObject({
          message: expect.stringContaining("Unauthorized"),
        })
        expect(mockRes.status).not.toHaveBeenCalled()
      })

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

        await expect(
          suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response),
        ).rejects.toMatchObject({
          message: expect.stringContaining("Installation error"),
        })
        expect(mockRes.status).not.toHaveBeenCalled()
      })

      it("should return 403 when user is not a collaborator", async () => {
        mockDependencies.userNickname.mockResolvedValue("test-user")
        mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
        mockDependencies.isUserCollaborator.mockResolvedValue(false)

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

        await expect(
          suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response),
        ).rejects.toMatchObject({
          message: expect.stringContaining("not a collaborator"),
        })
        expect(mockRes.status).not.toHaveBeenCalled()

        consoleSpy.mockRestore()
      })

    })

    describe("approval workflow", () => {
      beforeEach(() => {
        mockReq = {
          body: {
            owner: "test-owner",
            repo: "test-repo",
            pullNumber: 1,
            diffContents: [{ file: "test.js", content: "test" }],
            prCommentFields: { function_name: "testFunc" },
            traceId: "test-trace-id",
          },
          userId: "test-user-id",
        } as AuthorizedUserReq
        mockDependencies.isDiffContentsWellFormed.mockReturnValue(true)
        mockDependencies.userNickname.mockResolvedValue("test-user")

        // Create a properly typed mock installation octokit
        const mockInstallationOctokit = {
          rest: {
            pulls: {
              get: jest.fn() as any,
            },
          },
        }
        // Fix: Use any to bypass TypeScript issues
        mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
          data: { head: { ref: "feature-branch" } },
        })
        mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockInstallationOctokit)
        mockDependencies.isUserCollaborator.mockResolvedValue(true)

        // Set up common mocks for triggerSuggestPrChanges dependencies
        mockDependencies.fileDiffsToMap.mockReturnValue(new Map())
        mockDependencies.determineValidHunks.mockResolvedValue({
          validHunks: new Map(),
          invalidHunks: new Map(),
        })
        mockDependencies.buildDependentPrTitle.mockReturnValue("Test PR Title")
        mockDependencies.createNewBranchFromDiffContents.mockResolvedValue(true)
        mockDependencies.buildPrCommentBody.mockReturnValue("Test comment body")
      })

      it("should fallback to staging for rejected requests", async () => {
        mockDependencies.requiresApproval.mockReturnValue(true)
        mockDependencies.prisma.optimization_features.findUnique.mockResolvedValue({
          approval_status: "rejected",
        })
        mockDependencies.saveStagingReview.mockResolvedValue({
          status: 200,
          data: { stagingId: "test-staging-id" },
        })

        await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)

        expect(mockDependencies.saveStagingReview).toHaveBeenCalled()
        expect(mockRes.status).toHaveBeenCalledWith(200)
        expect(mockRes.json).toHaveBeenCalledWith({
          message: "PR suggestion failed, staging created as fallback",
          stagingId: "test-staging-id",
        })
      })

      it("should proceed with approved requests", async () => {
        mockDependencies.requiresApproval.mockReturnValue(true)
        mockDependencies.prisma.optimization_features.findUnique.mockResolvedValue({
          approval_status: "approved",
        })

        // Set up valid hunks with proper line ranges so triggerSuggestPrChanges can succeed
        const validHunks = new Map([
          [
            "file1.js",
            [{ oldStart: 1, oldEnd: 5, newContent: ["new code line 1", "new code line 2"] }],
          ],
        ])
        mockDependencies.determineValidHunks.mockResolvedValue({
          validHunks,
          invalidHunks: new Map(),
        })

        // Mock the installation octokit to also include createReview
        const mockInstallationOctokit = {
          rest: {
            pulls: {
              get: jest.fn() as any,
              createReview: jest.fn() as any,
            },
          },
        }
        mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
          data: { head: { ref: "feature-branch", sha: "abc123" } },
        })
        mockInstallationOctokit.rest.pulls.createReview.mockResolvedValue({
          data: {
            id: 789,
            html_url: "https://github.com/test/test/pull/1#pullrequestreview-789",
          },
        })
        mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockInstallationOctokit)

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

        await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)

        expect(consoleSpy).toHaveBeenCalledWith(
          expect.stringContaining(
            "Request test-trace-id was previously approved, continuing with PR suggestion",
          ),
        )
        expect(mockRes.json).toHaveBeenCalled()

        consoleSpy.mockRestore()
      })

      it("should request approval for new requests", async () => {
        mockDependencies.requiresApproval.mockReturnValue(true)
        mockDependencies.prisma.optimization_features.findUnique.mockResolvedValue({
          approval_status: "pending",
        })

        await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)

        expect(mockDependencies.requestApproval).toHaveBeenCalledWith(
          "test-trace-id",
          "test-owner",
          "test-repo",
          "testFunc",
          "test-user-id",
          expect.any(Object),
        )
        expect(mockRes.status).toHaveBeenCalledWith(202)
        expect(mockRes.json).toHaveBeenCalledWith({
          status: "pending_approval",
          message:
            "This PR suggestion requires approval. You will be notified when it is processed.",
        })
      })
    })

    describe("error handling", () => {
      it("should handle errors and track them", async () => {
        mockReq = {
          body: {
            owner: "test-owner",
            repo: "test-repo",
            pullNumber: 1,
            diffContents: [{ file: "test.js", content: "test" }],
          },
          userId: "test-user-id",
        } as AuthorizedUserReq

        const error = new Error("Test error")
        error.stack = "Test stack trace"
        mockDependencies.isDiffContentsWellFormed.mockReturnValue(true)
        mockDependencies.userNickname.mockRejectedValue(error)

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

        await expect(
          suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response),
        ).rejects.toMatchObject({
          message: expect.stringContaining("Error creating pull request"),
        })
        expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
          distinctId: "test-user-id",
          event: "cfapi-suggest-pr-changes-failed-error",
          properties: {
            error: error.message,
            stack: error.stack,
          },
        })
        expect(mockRes.status).not.toHaveBeenCalled()

        consoleErrorSpy.mockRestore()
      })
    })
  })

  describe("triggerSuggestPrChanges", () => {
    let mockInstallationOctokit: any

    beforeEach(() => {
      mockInstallationOctokit = {
        rest: {
          pulls: {
            get: jest.fn() as any,
            createReview: jest.fn() as any,
          },
          issues: {
            createComment: jest.fn() as any,
          },
        },
      }

      // Setup default mocks
      mockDependencies.fileDiffsToMap.mockReturnValue(new Map())
      mockDependencies.determineValidHunks.mockResolvedValue({
        validHunks: new Map(),
        invalidHunks: new Map(),
      })
      mockDependencies.buildDependentPrTitle.mockReturnValue("Test PR Title")
      mockDependencies.createNewBranchFromDiffContents.mockResolvedValue(true)
    })

    describe("dependent PR creation", () => {
      it("should create dependent PR when there are invalid hunks", async () => {
        const validHunks = new Map([
          ["file1.js", [{ oldStart: 1, oldEnd: 5, newContent: ["new code"] }]],
        ])
        const invalidHunks = new Map([
          ["file2.js", [{ oldStart: 10, oldEnd: 15, newContent: ["invalid code"] }]],
        ])

        mockDependencies.determineValidHunks.mockResolvedValue({
          validHunks,
          invalidHunks,
        })
        // Fix: Use any to bypass TypeScript issues
        mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
          data: { head: { ref: "feature-branch" } },
        })
        mockDependencies.createDependentPullRequest.mockResolvedValue({
          data: {
            id: 123,
            number: 456,
            html_url: "https://github.com/test/test/pull/456",
            head: { ref: "codeflash/optimize-pr1-2024-01-01" },
          },
        })

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

        const result = await triggerSuggestPrChanges(
          "test-owner",
          "test-repo",
          1,
          [],
          {
            function_name: "testFunc",
            speedup_pct: 50,
            speedup_x: 2,
            optimization_explanation: "test",
          },
          "existing tests",
          "generated tests",
          "coverage message",
          "user123",
          "testuser",
          mockInstallationOctokit,
          "replay tests",
          "concolic tests",
          "trace123",
        )

        expect(consoleSpy).toHaveBeenCalledWith(
          expect.stringContaining("Creating a dependent PR because there are invalid hunks."),
        )
        expect(consoleSpy).toHaveBeenCalledWith(
          expect.stringContaining("Making a new dependent PR..."),
        )
        expect(consoleSpy).toHaveBeenCalledWith(
          expect.stringContaining(
            "Created new dependent PR #456 from branch codeflash/optimize-pr1-2024-01-01",
          ),
        )
        expect(mockDependencies.createDependentPullRequest).toHaveBeenCalled()
        expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
          distinctId: "user123",
          event: "cfapi-suggest-pr-changes-success-dependent-pr-created",
          properties: {
            owner: "test-owner",
            repo: "test-repo",
            newPrNumber: 456,
            newPrBranch: "codeflash/optimize-pr1-2024-01-01",
            PRURL: "https://github.com/test/test/pull/456",
          },
        })
        expect(result).toBe(456)

        consoleSpy.mockRestore()
      })

      it("should create dependent PR when there are multiple files", async () => {
        const validHunks = new Map([
          ["file1.js", [{ oldStart: 1, oldEnd: 5, newContent: ["new code"] }]],
          ["file2.js", [{ oldStart: 10, oldEnd: 15, newContent: ["more code"] }]],
        ])
        const invalidHunks = new Map()

        mockDependencies.determineValidHunks.mockResolvedValue({
          validHunks,
          invalidHunks,
        })
        mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
          data: { head: { ref: "feature-branch" } },
        })
        mockDependencies.createDependentPullRequest.mockResolvedValue({
          data: {
            id: 123,
            number: 456,
            html_url: "https://github.com/test/test/pull/456",
            head: { ref: "codeflash/optimize-pr1-2024-01-01" },
          },
        })

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

        const result = await triggerSuggestPrChanges(
          "test-owner",
          "test-repo",
          1,
          [],
          {
            function_name: "testFunc",
            speedup_pct: 50,
            speedup_x: 2,
            optimization_explanation: "test",
          },
          "existing tests",
          "generated tests",
          "coverage message",
          "user123",
          "testuser",
          mockInstallationOctokit,
          "replay tests",
          "concolic tests",
          "trace123",
        )

        expect(consoleSpy).toHaveBeenCalledWith(
          expect.stringContaining(
            "Found 2 files with changes, using dependent PR instead of review comments",
          ),
        )
        expect(consoleSpy).toHaveBeenCalledWith(
          expect.stringContaining(
            "Creating a dependent PR because there are multiple valid hunks.",
          ),
        )
        expect(result).toBe(456)

        consoleSpy.mockRestore()
      })

      it("should send Slack notification for configured repositories", async () => {
        const validHunks = new Map([
          ["file1.js", [{ oldStart: 1, oldEnd: 5, newContent: ["new code"] }]],
        ])
        const invalidHunks = new Map([
          ["file2.js", [{ oldStart: 10, oldEnd: 15, newContent: ["invalid code"] }]],
        ])

        mockDependencies.determineValidHunks.mockResolvedValue({
          validHunks,
          invalidHunks,
        })
        mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
          data: { head: { ref: "feature-branch" } },
        })
        mockDependencies.createDependentPullRequest.mockResolvedValue({
          data: {
            id: 123,
            number: 456,
            html_url: "https://github.com/test/test/pull/456",
            head: { ref: "codeflash/optimize-pr1-2024-01-01" },
          },
        })

        await triggerSuggestPrChanges(
          "Future-House", // This is in slackNotificationConfig
          "aviary", // This repo is configured for notifications
          1,
          [],
          {
            function_name: "testFunc",
            speedup_pct: 50,
            speedup_x: 2,
            optimization_explanation: "test",
          },
          "existing tests",
          "generated tests",
          "coverage message",
          "user123",
          "testuser",
          mockInstallationOctokit,
          "replay tests",
          "concolic tests",
          "trace123",
        )

        expect(mockDependencies.sendSlackMessage).toHaveBeenCalledWith(
          "new dependent PR created: https://github.com/test/test/pull/456 for Future-House/aviary",
        )
      })
    })

    describe("review comment creation", () => {
      it("should create review comments for single valid hunk", async () => {
        const validHunks = new Map([
          [
            "file1.js",
            [{ oldStart: 1, oldEnd: 5, newContent: ["new code line 1", "new code line 2"] }],
          ],
        ])
        const invalidHunks = new Map()

        mockDependencies.determineValidHunks.mockResolvedValue({
          validHunks,
          invalidHunks,
        })
        mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
          data: {
            head: { ref: "feature-branch", sha: "abc123" },
          },
        })
        mockInstallationOctokit.rest.pulls.createReview.mockResolvedValue({
          data: {
            id: 789,
            html_url: "https://github.com/test/test/pull/1#pullrequestreview-789",
          },
        })
        mockDependencies.buildPrCommentBody.mockReturnValue("Test PR comment body")

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

        const result = await triggerSuggestPrChanges(
          "test-owner",
          "test-repo",
          1,
          [],
          {
            function_name: "testFunc",
            speedup_pct: 50,
            speedup_x: 2,
            optimization_explanation: "test",
          },
          "existing tests",
          "generated tests",
          "coverage message",
          "user123",
          "testuser",
          mockInstallationOctokit,
          "replay tests",
          "concolic tests",
          "trace123",
          "low",
        )

        expect(consoleSpy).toHaveBeenCalledWith(
          expect.stringContaining("Creating unified review for a single valid hunk."),
        )
        expect(consoleSpy).toHaveBeenCalledWith(
          expect.stringContaining(
            "Added review comment to PR #1: https://github.com/test/test/pull/1#pullrequestreview-789",
          ),
        )
        expect(mockInstallationOctokit.rest.pulls.createReview).toHaveBeenCalledWith({
          owner: "test-owner",
          repo: "test-repo",
          pull_number: 1,
          commit_id: "abc123",
          event: "COMMENT",
          comments: [
            {
              path: "file1.js",
              line: 5,
              start_line: 1,
              side: "RIGHT",
              body: "Test PR comment body\n\n```suggestion\nnew code line 1\nnew code line 2\n```\n\n\n![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-low-yellow)\n\n",
            },
          ],
        })
        expect(result).toBe(789)

        consoleSpy.mockRestore()
      })

      it("should handle review creation failure gracefully", async () => {
        const validHunks = new Map([
          [
            "file1.js",
            [{ oldStart: 5, oldEnd: 1, newContent: ["new code"] }], // Invalid range (start > end)
          ],
        ])
        const invalidHunks = new Map()

        mockDependencies.determineValidHunks.mockResolvedValue({
          validHunks,
          invalidHunks,
        })
        mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
          data: { head: { ref: "feature-branch", sha: "abc123" } },
        })
        mockDependencies.buildPrCommentBody.mockReturnValue("Test PR comment body")

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

        await expect(
          triggerSuggestPrChanges(
            "test-owner",
            "test-repo",
            1,
            [],
            {
              function_name: "testFunc",
              speedup_pct: 50,
              speedup_x: 2,
              optimization_explanation: "test",
            },
            "existing tests",
            "generated tests",
            "coverage message",
            "user123",
            "testuser",
            mockInstallationOctokit,
            "replay tests",
            "concolic tests",
            "trace123",
          ),
        ).rejects.toMatchObject({
          message: expect.stringContaining("Cannot create review comments"),
        })

        consoleLogSpy.mockRestore()
        consoleWarnSpy.mockRestore()
        consoleErrorSpy.mockRestore()
      })
    })

    describe("error handling", () => {
      it("should handle branch creation failure", async () => {
        // Set up validHunks with invalid hunks to trigger dependent PR creation
        const validHunks = new Map([
          ["file1.js", [{ oldStart: 1, oldEnd: 5, newContent: ["new code"] }]],
        ])
        const invalidHunks = new Map([
          ["file2.js", [{ oldStart: 10, oldEnd: 15, newContent: ["invalid code"] }]],
        ])

        mockDependencies.determineValidHunks.mockResolvedValue({
          validHunks,
          invalidHunks,
        })

        mockDependencies.createNewBranchFromDiffContents.mockResolvedValue(false)
        mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
          data: { head: { ref: "feature-branch" } },
        })

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

        await expect(
          triggerSuggestPrChanges(
            "test-owner",
            "test-repo",
            1,
            [],
            {
              function_name: "testFunc",
              speedup_pct: 50,
              speedup_x: 2,
              optimization_explanation: "test",
            },
            "existing tests",
            "generated tests",
            "coverage message",
            "user123",
            "testuser",
            mockInstallationOctokit,
            "replay tests",
            "concolic tests",
            "trace123",
            "low",
            mockRes as Response,
          ),
        ).rejects.toMatchObject({
          message: expect.stringContaining("Error creating pull request"),
        })
        expect(mockRes.status).not.toHaveBeenCalled()

        consoleErrorSpy.mockRestore()
      })
    })
  })
})
