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

let getStagingCode: typeof import("../get-staging-code").default
let retrieveStagingCode: typeof import("../get-staging-code").retrieveStagingCode
let setGetStagingCodeDependencies: typeof import("../get-staging-code").setGetStagingCodeDependencies
let resetGetStagingCodeDependencies: typeof import("../get-staging-code").resetGetStagingCodeDependencies

describe("getStagingCode", () => {
  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("../get-staging-code")
    getStagingCode = mod.default
    retrieveStagingCode = mod.retrieveStagingCode
    setGetStagingCodeDependencies = mod.setGetStagingCodeDependencies
    resetGetStagingCodeDependencies = mod.resetGetStagingCodeDependencies
  })

  beforeEach(() => {
    mockOctokit = {
      rest: {
        repos: {
          compareCommits: jest.fn(),
          getContent: jest.fn(),
        },
      },
    }

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

    mockDependencies.getInstallationOctokit.mockResolvedValue(mockOctokit)

    mockReq = {
      body: {
        stagingBranchName: "codeflash-staging/test-branch",
        baseBranch: "main",
        fullRepoName: "owner/repo",
        installationId: 123,
        functionName: "testFunc",
        filePath: "/path/to/file.ts",
      },
      userId: "test-user-id",
    }

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

    setGetStagingCodeDependencies(mockDependencies)
  })

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

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

      await getStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(400)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Missing required fields: stagingBranchName, fullRepoName, installationId",
      })
    })

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

      await getStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(400)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Missing required fields: stagingBranchName, fullRepoName, installationId",
      })
    })

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

      await getStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(400)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Missing required fields: stagingBranchName, fullRepoName, installationId",
      })
    })

    it("should return 400 when fullRepoName has invalid format", async () => {
      mockReq.body.fullRepoName = "invalid-format"
      mockDependencies.userNickname.mockResolvedValue("test-user")

      await getStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(400)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Invalid fullRepoName format, expected 'owner/repo'",
      })
    })
  })

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

      await getStagingCode(mockReq, mockRes)

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

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

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

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

      await getStagingCode(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",
      })

      consoleSpy.mockRestore()
    })
  })

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

    it("should return 200 with diffContents on success", async () => {
      mockOctokit.rest.repos.compareCommits.mockResolvedValue({
        data: {
          files: [
            { filename: "src/file.ts", status: "modified" },
            { filename: "src/other.ts", status: "added" },
          ],
        },
      })
      // Mock based on path and ref to handle parallel calls correctly
      mockOctokit.rest.repos.getContent.mockImplementation(({ path, ref }: { path: string; ref: string }) => {
        if (path === "src/file.ts" && ref === "codeflash-staging/test-branch") {
          return Promise.resolve({ data: { content: Buffer.from("new content 1").toString("base64") } })
        }
        if (path === "src/file.ts" && ref === "main") {
          return Promise.resolve({ data: { content: Buffer.from("old content 1").toString("base64") } })
        }
        if (path === "src/other.ts" && ref === "codeflash-staging/test-branch") {
          return Promise.resolve({ data: { content: Buffer.from("new content 2").toString("base64") } })
        }
        return Promise.reject(new Error("Not found"))
      })

      await getStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(200)
      expect(mockRes.json).toHaveBeenCalledWith({
        storageType: "git_branch",
        diffContents: {
          "src/file.ts": {
            oldContent: "old content 1",
            newContent: "new content 1",
          },
          "src/other.ts": {
            oldContent: "",
            newContent: "new content 2",
          },
        },
        stagingBranchName: "codeflash-staging/test-branch",
        baseBranch: "main",
        functionName: "testFunc",
        filePath: "/path/to/file.ts",
      })
    })

    it("should use main as default when baseBranch is empty", async () => {
      mockReq.body.baseBranch = ""
      mockOctokit.rest.repos.compareCommits.mockResolvedValue({
        data: { files: [] },
      })

      await getStagingCode(mockReq, mockRes)

      expect(mockOctokit.rest.repos.compareCommits).toHaveBeenCalledWith({
        owner: "owner",
        repo: "repo",
        base: "main",
        head: "codeflash-staging/test-branch",
      })
    })

    it("should skip deleted files", async () => {
      mockOctokit.rest.repos.compareCommits.mockResolvedValue({
        data: {
          files: [
            { filename: "src/file.ts", status: "modified" },
            { filename: "src/deleted.ts", status: "removed" },
          ],
        },
      })
      // Mock based on path and ref to handle parallel calls correctly
      mockOctokit.rest.repos.getContent.mockImplementation(({ path, ref }: { path: string; ref: string }) => {
        if (path === "src/file.ts" && ref === "codeflash-staging/test-branch") {
          return Promise.resolve({ data: { content: Buffer.from("new content").toString("base64") } })
        }
        if (path === "src/file.ts" && ref === "main") {
          return Promise.resolve({ data: { content: Buffer.from("old content").toString("base64") } })
        }
        return Promise.reject(new Error("Not found"))
      })

      await getStagingCode(mockReq, mockRes)

      // Should fetch staging + base for modified file only (2 calls)
      expect(mockOctokit.rest.repos.getContent).toHaveBeenCalledTimes(2)
      expect(mockRes.json).toHaveBeenCalledWith(
        expect.objectContaining({
          diffContents: {
            "src/file.ts": {
              oldContent: "old content",
              newContent: "new content",
            },
          },
        }),
      )
    })
  })

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

    it("should return 500 on unexpected error", async () => {
      mockDependencies.isUserCollaborator.mockRejectedValue(new Error("GitHub API error"))

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

      await getStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(500)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Failed to retrieve staging code: GitHub API error",
      })

      consoleErrorSpy.mockRestore()
    })

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

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

      await getStagingCode(mockReq, mockRes)

      expect(mockRes.status).toHaveBeenCalledWith(500)
      expect(mockRes.json).toHaveBeenCalledWith({
        error: "Failed to retrieve staging code: Unknown error",
      })

      consoleErrorSpy.mockRestore()
    })
  })
})

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

  beforeAll(async () => {
    const mod = await import("../get-staging-code")
    setGetStagingCodeDependencies = mod.setGetStagingCodeDependencies
    resetGetStagingCodeDependencies = mod.resetGetStagingCodeDependencies
  })

  beforeEach(async () => {
    const mod = await import("../get-staging-code")
    retrieveStagingCode = mod.retrieveStagingCode

    mockOctokit = {
      rest: {
        repos: {
          compareCommits: jest.fn(),
          getContent: jest.fn(),
        },
      },
    }

    mockOctokit.rest.repos.compareCommits.mockResolvedValue({ data: { files: [] } })

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

    mockDependencies.getInstallationOctokit.mockResolvedValue(mockOctokit)

    setGetStagingCodeDependencies(mockDependencies)
  })

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

  it("should return correct result structure on success", async () => {
    mockDependencies.userNickname.mockResolvedValue("test-user")
    mockDependencies.isUserCollaborator.mockResolvedValue(true)

    const result = await retrieveStagingCode(
      {
        stagingBranchName: "test-branch",
        baseBranch: "develop",
        fullRepoName: "owner/repo",
        installationId: 123,
        functionName: "myFunction",
        filePath: "/src/index.ts",
      },
      "user-123",
    )

    expect(result.status).toBe(200)
    expect(result.data).toEqual({
      storageType: "git_branch",
      diffContents: {},
      stagingBranchName: "test-branch",
      baseBranch: "develop",
      functionName: "myFunction",
      filePath: "/src/index.ts",
    })
  })
})
