import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import {
  createNewBranchFromDiffContents,
  createNewPullRequest,
  assignReviewer,
  createStandalonePullRequest,
  createDependentPullRequest,
  setCreatePrFromDiffContentsDependencies,
  resetCreatePrFromDiffContentsDependencies,
  type PrCommentFields,
} from "../create-pr-from-diffcontents"
import { type FileDiffContent } from "@codeflash-ai/code-suggester/build/src/types.js"

describe("Create PR from Diff Contents", () => {
  let mockOctokit: any
  let mockDependencies: any

  beforeEach(() => {
    // Setup mock octokit
    mockOctokit = {
      rest: {
        git: {
          createRef: jest.fn(),
          getRef: jest.fn(),
          createBlob: jest.fn(),
          getCommit: jest.fn(),
          createTree: jest.fn(),
          createCommit: jest.fn(),
          updateRef: jest.fn(),
        },
        pulls: {
          create: jest.fn(),
          requestReviewers: jest.fn(),
        },
        issues: {
          createComment: jest.fn(),
        },
        repos: {
          getContent: jest.fn(),
        },
      },
    }

    // Setup mock dependencies
    mockDependencies = {
      addLabelToPullRequest: jest.fn(),
      buildBenchmarkInfo: jest.fn(),
      buildDependentPrTitle: jest.fn(),
      buildPrTitle: jest.fn(),
      buildResultFooter: jest.fn(),
      buildResultHeader: jest.fn(),
      buildResultDetails: jest.fn(),
      buildResultTestReport: jest.fn(),
      originalPRComment: jest.fn(),
    }

    setCreatePrFromDiffContentsDependencies(mockDependencies)
  })

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

  describe("createNewBranchFromDiffContents", () => {
    const diffContentsMap = new Map<string, FileDiffContent>([
      [
        "file1.js",
        {
          oldContent: "console.log('old code')",
          newContent: "console.log('optimized code')",
        },
      ],
      [
        "file2.js",
        {
          oldContent: "function old() { return false; }",
          newContent: "function optimized() { return true; }",
        },
      ],
    ])

    // Helper function to setup successful API responses
    const setupSuccessfulMocks = () => {
      mockOctokit.rest.git.getRef
        .mockResolvedValueOnce({ data: { object: { sha: "base-sha" } } })
        .mockResolvedValueOnce({ data: { object: { sha: "branch-sha" } } })
      mockOctokit.rest.git.createRef.mockResolvedValue({})
      mockOctokit.rest.git.createBlob
        .mockResolvedValueOnce({ data: { sha: "blob1-sha" } })
        .mockResolvedValueOnce({ data: { sha: "blob2-sha" } })
      mockOctokit.rest.git.getCommit.mockResolvedValue({
        data: { tree: { sha: "tree-sha" } },
      })
      mockOctokit.rest.git.createTree.mockResolvedValue({
        data: { sha: "new-tree-sha" },
      })
      mockOctokit.rest.git.createCommit.mockResolvedValue({
        data: { sha: "new-commit-sha" },
      })
      mockOctokit.rest.git.updateRef.mockResolvedValue({
        status: 200,
      })
      mockOctokit.rest.repos.getContent.mockResolvedValue({
        status: 200,
      })
    }

    it("should successfully create a new branch with diff contents", async () => {
      setupSuccessfulMocks()

      const result = await createNewBranchFromDiffContents(
        mockOctokit,
        "test-owner",
        "test-repo",
        "feature-branch",
        "main",
        diffContentsMap,
        "Optimize performance",
      )

      expect(result).toBe(true)

      // Verify branch creation
      expect(mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        ref: "refs/heads/feature-branch",
        sha: "base-sha",
      })

      // Verify blob creation for each file
      expect(mockOctokit.rest.git.createBlob).toHaveBeenCalledTimes(2)
      expect(mockOctokit.rest.git.createBlob).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        content: "console.log('optimized code')",
        encoding: "utf-8",
      })
      expect(mockOctokit.rest.git.createBlob).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        content: "function optimized() { return true; }",
        encoding: "utf-8",
      })

      // Verify tree creation
      expect(mockOctokit.rest.git.createTree).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        base_tree: "tree-sha",
        tree: [
          {
            path: "file1.js",
            mode: "100644",
            type: "blob",
            sha: "blob1-sha",
          },
          {
            path: "file2.js",
            mode: "100644",
            type: "blob",
            sha: "blob2-sha",
          },
        ],
      })

      // Verify commit creation
      expect(mockOctokit.rest.git.createCommit).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        message: "Optimize performance",
        tree: "new-tree-sha",
        parents: ["branch-sha"],
      })

      // Verify branch update
      expect(mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        ref: "heads/feature-branch",
        sha: "new-commit-sha",
      })
    })

    it("should handle empty diff contents map", async () => {
      setupSuccessfulMocks()

      const emptyMap = new Map<string, FileDiffContent>()

      const result = await createNewBranchFromDiffContents(
        mockOctokit,
        "test-owner",
        "test-repo",
        "feature-branch",
        "main",
        emptyMap,
        "Empty commit",
      )

      expect(result).toBe(true)
      expect(mockOctokit.rest.git.createBlob).not.toHaveBeenCalled()
      expect(mockOctokit.rest.git.createTree).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        base_tree: "tree-sha",
        tree: [],
      })
    })

    it("should return false when GitHub API fails", async () => {
      mockOctokit.rest.git.createRef.mockRejectedValue(new Error("API Error"))

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

      const result = await createNewBranchFromDiffContents(
        mockOctokit,
        "test-owner",
        "test-repo",
        "feature-branch",
        "main",
        diffContentsMap,
        "Optimize performance",
      )

      expect(result).toBe(false)
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        "Error creating branch from diff contents:",
        expect.any(Error),
      )

      consoleErrorSpy.mockRestore()
    })

    it("should return false when updateRef returns non-200 status", async () => {
      // Setup most mocks to succeed
      mockOctokit.rest.git.getRef
        .mockResolvedValueOnce({ data: { object: { sha: "base-sha" } } })
        .mockResolvedValueOnce({ data: { object: { sha: "branch-sha" } } })
      mockOctokit.rest.git.createRef.mockResolvedValue({})
      mockOctokit.rest.git.createBlob
        .mockResolvedValueOnce({ data: { sha: "blob1-sha" } })
        .mockResolvedValueOnce({ data: { sha: "blob2-sha" } })
      mockOctokit.rest.git.getCommit.mockResolvedValue({
        data: { tree: { sha: "tree-sha" } },
      })
      mockOctokit.rest.git.createTree.mockResolvedValue({
        data: { sha: "new-tree-sha" },
      })
      mockOctokit.rest.git.createCommit.mockResolvedValue({
        data: { sha: "new-commit-sha" },
      })

      // Only updateRef fails with non-200 status
      mockOctokit.rest.git.updateRef.mockResolvedValue({
        status: 400,
      })

      const result = await createNewBranchFromDiffContents(
        mockOctokit,
        "test-owner",
        "test-repo",
        "feature-branch",
        "main",
        diffContentsMap,
        "Optimize performance",
      )

      expect(result).toBe(false)
    })
  })

  describe("createNewPullRequest", () => {
    it("should create a new pull request successfully", async () => {
      const mockPrResponse = {
        data: {
          id: 123,
          number: 456,
          html_url: "https://github.com/test-owner/test-repo/pull/456",
          title: "Test PR",
        },
      }

      mockOctokit.rest.pulls.create.mockResolvedValue(mockPrResponse)

      const result = await createNewPullRequest(
        mockOctokit,
        "test-owner",
        "test-repo",
        "Test PR Title",
        "Test PR Body",
        "feature-branch",
        "main",
      )

      expect(result).toEqual(mockPrResponse)
      expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        title: "Test PR Title",
        head: "feature-branch",
        base: "main",
        body: "Test PR Body",
      })
    })

    it("should propagate GitHub API errors", async () => {
      const error = new Error("PR creation failed")
      mockOctokit.rest.pulls.create.mockRejectedValue(error)

      await expect(
        createNewPullRequest(
          mockOctokit,
          "test-owner",
          "test-repo",
          "Test PR Title",
          "Test PR Body",
          "feature-branch",
          "main",
        ),
      ).rejects.toThrow("PR creation failed")
    })
  })

  describe("assignReviewer", () => {
    it("should assign a reviewer to a pull request", async () => {
      await assignReviewer(mockOctokit, "test-owner", "test-repo", 456, "reviewer-username")

      expect(mockOctokit.rest.pulls.requestReviewers).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        pull_number: 456,
        reviewers: ["reviewer-username"],
      })
    })

    it("should handle GitHub API errors when assigning reviewer", async () => {
      const error = new Error("Review request failed")
      mockOctokit.rest.pulls.requestReviewers.mockRejectedValue(error)

      await expect(
        assignReviewer(mockOctokit, "test-owner", "test-repo", 456, "reviewer-username"),
      ).rejects.toThrow("Review request failed")
    })
  })

  describe("createStandalonePullRequest", () => {
    let mockPrCommentFields: PrCommentFields

    beforeEach(() => {
      mockPrCommentFields = {
        best_runtime: "10ms",
        original_runtime: "20ms",
        function_name: "testFunction",
        file_path: "test.js",
        speedup_x: "2.0",
        speedup_pct: "50",
        loop_count: "1000",
        optimization_explanation: "Optimized loop",
        report_table: {},
        benchmark_details: [],
      }

      // Setup mock dependencies
      mockDependencies.buildResultHeader.mockReturnValue("## Header")
      mockDependencies.buildResultDetails.mockReturnValue("Details section")
      mockDependencies.buildResultTestReport.mockReturnValue("Test report")
      mockDependencies.buildResultFooter.mockReturnValue("Footer")
      mockDependencies.buildPrTitle.mockReturnValue("Optimize testFunction")
      mockOctokit.rest.pulls.create.mockResolvedValue({
        data: { id: 123, number: 456 },
      })
    })

    it("should create standalone PR without benchmark info", async () => {
      const result = await createStandalonePullRequest(
        mockOctokit,
        "test-owner",
        "test-repo",
        "feature-branch",
        "main",
        mockPrCommentFields,
        "existing tests",
        '```python:myfile.py\nprint("Hello, World!")\n```\n\n```python:myfile.py\nprint("Hello, World!")\n```',
        "coverage message",
        "replay tests",
        "concolic tests",
      )

      expect(mockDependencies.buildResultHeader).toHaveBeenCalledWith(mockPrCommentFields)
      expect(mockDependencies.buildResultDetails).toHaveBeenCalledWith(mockPrCommentFields)
      expect(mockDependencies.buildResultTestReport).toHaveBeenCalledWith(
        mockPrCommentFields,
        "existing tests",
        '```python:myfile.py\nprint("Hello, World!")\n```\n\n```python:myfile.py\nprint("Hello, World!")\n```',
        "coverage message",
        "replay tests",
        "concolic tests",
      )
      expect(mockDependencies.buildResultFooter).toHaveBeenCalledWith("feature-branch")
      expect(mockDependencies.buildPrTitle).toHaveBeenCalledWith("testFunction", "50", "2.0")

      expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        title: "Optimize testFunction",
        head: "feature-branch",
        base: "main",
        body: "## Header\nDetails section\nTest report\nFooter",
      })

      expect(result.data.id).toBe(123)
    })

    it("should create standalone PR with benchmark info", async () => {
      mockPrCommentFields.benchmark_details = [
        {
          benchmark_name: "test_benchmark",
          test_function: "testFunction",
          original_timing: 20,
          expected_new_timing: 10,
          speedup_percent: 50,
        },
      ]
      mockDependencies.buildBenchmarkInfo.mockReturnValue("Benchmark info")

      await createStandalonePullRequest(
        mockOctokit,
        "test-owner",
        "test-repo",
        "feature-branch",
        "main",
        mockPrCommentFields,
        "existing tests",
        "generated tests",
        "coverage message",
        "replay tests",
        "concolic tests",
      )

      expect(mockDependencies.buildBenchmarkInfo).toHaveBeenCalledWith(mockPrCommentFields)
      expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        title: "Optimize testFunction",
        head: "feature-branch",
        base: "main",
        body: "## Header\nBenchmark info\nDetails section\nTest report\nFooter",
      })
    })
  })

  describe("createDependentPullRequest", () => {
    let mockPrCommentFields: PrCommentFields

    beforeEach(() => {
      mockPrCommentFields = {
        best_runtime: "10ms",
        original_runtime: "20ms",
        function_name: "testFunction",
        file_path: "test.js",
        speedup_x: "2.0",
        speedup_pct: "50",
        loop_count: "1000",
        optimization_explanation: "Optimized loop",
        report_table: {},
        benchmark_details: [],
      }

      // Setup mock dependencies
      mockDependencies.buildResultHeader.mockReturnValue("## Header")
      mockDependencies.buildResultDetails.mockReturnValue("Details section")
      mockDependencies.buildResultTestReport.mockReturnValue("Test report")
      mockDependencies.buildResultFooter.mockReturnValue("Footer")
      mockDependencies.buildDependentPrTitle.mockReturnValue("Dependent PR: Optimize testFunction")
      mockDependencies.originalPRComment.mockReturnValue("Original PR comment")
      mockOctokit.rest.pulls.create.mockResolvedValue({
        data: { id: 123, number: 456 },
      })
    })

    it("should create dependent PR and comment on original PR", async () => {
      const result = await createDependentPullRequest(
        mockOctokit,
        "test-owner",
        "test-repo",
        789,
        "feature-branch",
        "pr-789-branch",
        mockPrCommentFields,
        "existing tests",
        "generated tests",
        "coverage message",
        "replay tests",
        "concolic tests",
        "medium",
      )

      expect(mockDependencies.buildDependentPrTitle).toHaveBeenCalledWith(
        "testFunction",
        "50",
        "2.0",
        789,
        "pr-789-branch",
      )

      // Check PR creation
      expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        title: "Dependent PR: Optimize testFunction",
        head: "feature-branch",
        base: "pr-789-branch",
        body: expect.stringContaining(
          "## ⚡️ This pull request contains optimizations for PR #789",
        ),
      })

      // Check label addition
      expect(mockDependencies.addLabelToPullRequest).toHaveBeenCalledWith(
        mockOctokit,
        "test-owner",
        "test-repo",
        456,
      )

      // Check comment on original PR
      expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledWith({
        owner: "test-owner",
        repo: "test-repo",
        issue_number: 789,
        body: "Original PR comment",
      })

      // Verify originalPRComment was called with correct parameters
      expect(mockDependencies.originalPRComment).toHaveBeenCalledWith(
        mockPrCommentFields,
        456,
        "pr-789-branch",
        "medium",
      )

      expect(result.data.id).toBe(123)
    })

    it("should handle GitHub API errors during PR creation", async () => {
      const error = new Error("PR creation failed")
      mockOctokit.rest.pulls.create.mockRejectedValue(error)

      await expect(
        createDependentPullRequest(
          mockOctokit,
          "test-owner",
          "test-repo",
          789,
          "feature-branch",
          "pr-789-branch",
          mockPrCommentFields,
          "existing tests",
          "generated tests",
          "coverage message",
          "replay tests",
          "concolic tests",
        ),
      ).rejects.toThrow("PR creation failed")
    })

    it("should handle GitHub API errors during comment creation", async () => {
      mockOctokit.rest.pulls.create.mockResolvedValue({
        data: { id: 123, number: 456 },
      })
      mockOctokit.rest.issues.createComment.mockRejectedValue(new Error("Comment creation failed"))

      await expect(
        createDependentPullRequest(
          mockOctokit,
          "test-owner",
          "test-repo",
          789,
          "feature-branch",
          "pr-789-branch",
          mockPrCommentFields,
          "existing tests",
          "generated tests",
          "coverage message",
          "replay tests",
          "concolic tests",
        ),
      ).rejects.toThrow("Comment creation failed")
    })
  })
})
