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

let commitStagingCode: typeof import("../commit-staging-code").default
let executeCommitStagingCode: typeof import("../commit-staging-code").executeCommitStagingCode
let setCommitStagingCodeDependencies: typeof import("../commit-staging-code").setCommitStagingCodeDependencies
let resetCommitStagingCodeDependencies: typeof import("../commit-staging-code").resetCommitStagingCodeDependencies

describe("commitStagingCode", () => {
  let mockReq: any
  let mockRes: any
  let mockDependencies: any
  let mockOctokit: any

  beforeAll(async () => {
    process.env.KEY_VAULT_NAME = "mocked-keyvault-name"
    const mod = await import("../commit-staging-code")
    commitStagingCode = mod.default
    executeCommitStagingCode = mod.executeCommitStagingCode
    setCommitStagingCodeDependencies = mod.setCommitStagingCodeDependencies
    resetCommitStagingCodeDependencies = mod.resetCommitStagingCodeDependencies
  })

  beforeEach(() => {
    mockOctokit = {
      rest: {
        git: {
          getRef: jest.fn(),
          getCommit: jest.fn(),
          createBlob: jest.fn(),
          createTree: jest.fn(),
          createCommit: jest.fn(),
          updateRef: jest.fn(),
        },
      },
    }

    mockDependencies = {
      prisma: {
        optimization_events: {
          findFirst: jest.fn(),
        },
      },
      getInstallationOctokit: jest.fn(),
      userNickname: jest.fn(),
      isUserCollaborator: jest.fn(),
    }

    // Set mock return value for getInstallationOctokit
    mockDependencies.getInstallationOctokit.mockResolvedValue(mockOctokit)

    mockReq = {
      body: {
        traceId: "test-trace-id",
        fileChanges: {
          "src/file.ts": "new content",
        },
        commitMessage: "Test commit message",
      },
      userId: "test-user-id",
    }

    mockRes = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    }

    setCommitStagingCodeDependencies(mockDependencies)
  })

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

  describe("input validation", () => {
    it("should return 401 when userId is missing", async () => {
      mockReq.userId = undefined

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(401)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Unauthorized - user ID is required",
      })
    })

    it("should return 400 when traceId is missing", async () => {
      mockReq.body.traceId = undefined

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(400)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Missing traceId in request body",
      })
    })

    it("should return 400 when fileChanges is missing", async () => {
      mockReq.body.fileChanges = undefined

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(400)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Missing fileChanges in request body",
      })
    })

    it("should return 400 when fileChanges is empty", async () => {
      mockReq.body.fileChanges = {}

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(400)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Missing fileChanges in request body",
      })
    })
  })

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

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(401)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Unauthorized - could not get user nickname",
      })
    })
  })

  describe("staging event lookup", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
    })

    it("should return 404 when no staging event is found", async () => {
      mockDependencies.prisma.optimization_events.findFirst.mockResolvedValue(null)

      await commitStagingCode(mockReq, mockRes)

      expect(mockDependencies.prisma.optimization_events.findFirst).toHaveBeenCalledWith({
        where: {
          trace_id: "test-trace-id",
          user_id: "test-user-id",
          is_staging: true,
          staging_storage_type: "git_branch",
        },
        include: {
          repository: true,
        },
      })
      expect(mockRes.status).toHaveBeenCalledWith(404)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "No git_branch staging event found for the given traceId",
      })
    })

    it("should return 404 when staging branch name is missing from metadata", async () => {
      mockDependencies.prisma.optimization_events.findFirst.mockResolvedValue({
        metadata: {},
        repository: { full_name: "owner/repo", installation_id: 123 },
      })

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(404)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "No staging branch name found in metadata",
      })
    })

    it("should return 404 when repository is missing", async () => {
      mockDependencies.prisma.optimization_events.findFirst.mockResolvedValue({
        metadata: { staging_branch_name: "test-branch" },
        repository: null,
      })

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(404)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "No repository or installation found for this staging event",
      })
    })

    it("should return 404 when installation_id is missing", async () => {
      mockDependencies.prisma.optimization_events.findFirst.mockResolvedValue({
        metadata: { staging_branch_name: "test-branch" },
        repository: { full_name: "owner/repo", installation_id: null },
      })

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(404)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "No repository or installation found for this staging event",
      })
    })
  })

  describe("authorization", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.prisma.optimization_events.findFirst.mockResolvedValue({
        metadata: { staging_branch_name: "codeflash-staging/test-branch" },
        repository: { full_name: "owner/repo", installation_id: 123 },
      })
    })

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

      await commitStagingCode(mockReq, mockRes)

      expect(mockDependencies.isUserCollaborator).toHaveBeenCalledWith(
        mockOctokit,
        "owner",
        "repo",
        "test-user",
      )
      expect(mockRes.status).toHaveBeenCalledWith(403)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "User test-user is not a collaborator on owner/repo",
        nickname: "test-user",
      })
    })
  })

  describe("successful commit", () => {
    beforeEach(() => {
      mockDependencies.userNickname.mockResolvedValue("test-user")
      mockDependencies.prisma.optimization_events.findFirst.mockResolvedValue({
        metadata: { staging_branch_name: "codeflash-staging/test-branch" },
        repository: { full_name: "owner/repo", installation_id: 123 },
      })
      mockDependencies.isUserCollaborator.mockResolvedValue(true)

      // Setup mock octokit responses for commit flow
      mockOctokit.rest.git.getRef.mockResolvedValue({
        data: { object: { sha: "current-commit-sha" } },
      })
      mockOctokit.rest.git.getCommit.mockResolvedValue({
        data: { tree: { sha: "current-tree-sha" } },
      })
      mockOctokit.rest.git.createBlob.mockResolvedValue({
        data: { sha: "new-blob-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({})
    })

    it("should return 200 with commit details on success", async () => {
      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockRes.json).toHaveBeenCalledWith({
        message: "Changes committed successfully",
        commitSha: "new-commit-sha",
        stagingBranchName: "codeflash-staging/test-branch",
      })
    })

    it("should use custom commit message when provided", async () => {
      mockReq.body.commitMessage = "Custom commit message"

      await commitStagingCode(mockReq, mockRes)

      expect(mockOctokit.rest.git.createCommit).toHaveBeenCalledWith({
        owner: "owner",
        repo: "repo",
        message: "Custom commit message",
        tree: "new-tree-sha",
        parents: ["current-commit-sha"],
        author: {
          name: "test-user",
          email: "test-user@users.noreply.github.com",
        },
      })
    })

    it("should use default commit message when not provided", async () => {
      delete mockReq.body.commitMessage

      await commitStagingCode(mockReq, mockRes)

      expect(mockOctokit.rest.git.createCommit).toHaveBeenCalledWith({
        owner: "owner",
        repo: "repo",
        message: "Update optimized code via Codeflash",
        tree: "new-tree-sha",
        parents: ["current-commit-sha"],
        author: {
          name: "test-user",
          email: "test-user@users.noreply.github.com",
        },
      })
    })

    it("should create blobs for all file changes", async () => {
      mockReq.body.fileChanges = {
        "src/file1.ts": "content 1",
        "src/file2.ts": "content 2",
        "src/file3.ts": "content 3",
      }

      mockOctokit.rest.git.createBlob
        .mockResolvedValueOnce({ data: { sha: "blob-sha-1" } })
        .mockResolvedValueOnce({ data: { sha: "blob-sha-2" } })
        .mockResolvedValueOnce({ data: { sha: "blob-sha-3" } })

      await commitStagingCode(mockReq, mockRes)

      expect(mockOctokit.rest.git.createBlob).toHaveBeenCalledTimes(3)
      expect(mockOctokit.rest.git.createTree).toHaveBeenCalledWith({
        owner: "owner",
        repo: "repo",
        base_tree: "current-tree-sha",
        tree: expect.arrayContaining([
          expect.objectContaining({ path: "src/file1.ts", sha: "blob-sha-1" }),
          expect.objectContaining({ path: "src/file2.ts", sha: "blob-sha-2" }),
          expect.objectContaining({ path: "src/file3.ts", sha: "blob-sha-3" }),
        ]),
      })
    })

    it("should get branch reference with correct branch name", async () => {
      await commitStagingCode(mockReq, mockRes)

      expect(mockOctokit.rest.git.getRef).toHaveBeenCalledWith({
        owner: "owner",
        repo: "repo",
        ref: "heads/codeflash-staging/test-branch",
      })
    })

    it("should update branch reference after commit", async () => {
      await commitStagingCode(mockReq, mockRes)

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

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

    it("should return 500 on database error", async () => {
      mockDependencies.prisma.optimization_events.findFirst.mockRejectedValue(
        new Error("Database error"),
      )

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

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(500)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Failed to commit changes: Database error",
      })

      consoleErrorSpy.mockRestore()
    })

    it("should return 500 on GitHub API error", async () => {
      mockDependencies.prisma.optimization_events.findFirst.mockResolvedValue({
        metadata: { staging_branch_name: "codeflash-staging/test-branch" },
        repository: { full_name: "owner/repo", installation_id: 123 },
      })
      mockDependencies.isUserCollaborator.mockResolvedValue(true)
      mockOctokit.rest.git.getRef.mockRejectedValue(new Error("Branch not found"))

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

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(500)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Failed to commit changes: Branch not found",
      })

      consoleErrorSpy.mockRestore()
    })

    it("should handle non-Error exceptions", async () => {
      mockDependencies.prisma.optimization_events.findFirst.mockRejectedValue("String error")

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

      await commitStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(500)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Failed to commit changes: Unknown error",
      })

      consoleErrorSpy.mockRestore()
    })
  })
})

describe("executeCommitStagingCode function", () => {
  let mockDependencies: any
  let mockOctokit: any

  beforeAll(async () => {
    const mod = await import("../commit-staging-code")
    executeCommitStagingCode = mod.executeCommitStagingCode
    setCommitStagingCodeDependencies = mod.setCommitStagingCodeDependencies
    resetCommitStagingCodeDependencies = mod.resetCommitStagingCodeDependencies
  })

  beforeEach(() => {
    mockOctokit = {
      rest: {
        git: {
          getRef: jest.fn(),
          getCommit: jest.fn(),
          createBlob: jest.fn(),
          createTree: jest.fn(),
          createCommit: jest.fn(),
          updateRef: jest.fn(),
        },
      },
    }

    // Set mock return values
    mockOctokit.rest.git.getRef.mockResolvedValue({ data: { object: { sha: "sha" } } })
    mockOctokit.rest.git.getCommit.mockResolvedValue({ data: { tree: { sha: "tree-sha" } } })
    mockOctokit.rest.git.createBlob.mockResolvedValue({ data: { sha: "blob-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({})

    mockDependencies = {
      prisma: {
        optimization_events: {
          findFirst: jest.fn(),
        },
      },
      getInstallationOctokit: jest.fn(),
      userNickname: jest.fn(),
      isUserCollaborator: jest.fn(),
    }

    mockDependencies.getInstallationOctokit.mockResolvedValue(mockOctokit)

    setCommitStagingCodeDependencies(mockDependencies)
  })

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

  it("should return correct result structure on success", async () => {
    mockDependencies.userNickname.mockResolvedValue("test-user")
    mockDependencies.prisma.optimization_events.findFirst.mockResolvedValue({
      metadata: { staging_branch_name: "test-branch" },
      repository: { full_name: "owner/repo", installation_id: 123 },
    })
    mockDependencies.isUserCollaborator.mockResolvedValue(true)

    const result = await executeCommitStagingCode(
      {
        traceId: "trace-123",
        fileChanges: { "file.ts": "content" },
      },
      "user-123",
    )

    expect(result.status).toBe(200)
    expect(result.data).toEqual({
      message: "Changes committed successfully",
      commitSha: "new-commit-sha",
      stagingBranchName: "test-branch",
    })
  })

  it("should return 401 when userId is undefined", async () => {
    const result = await executeCommitStagingCode(
      {
        traceId: "trace-123",
        fileChanges: { "file.ts": "content" },
      },
      undefined,
    )

    expect(result.status).toBe(401)
    expect(result.data).toEqual({ error: "Unauthorized - user ID is required" })
  })
})
