/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
import { GitService } from './gitService.js';
import { Storage } from '../config/storage.js';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import { getProjectHash, GEMINI_DIR } from '../utils/paths.js';
import { spawnAsync } from '../utils/shell-utils.js';
vi.mock('../utils/shell-utils.js', () => ({
    spawnAsync: vi.fn(),
}));
const hoistedMockEnv = vi.hoisted(() => vi.fn());
const hoistedMockSimpleGit = vi.hoisted(() => vi.fn());
const hoistedMockCheckIsRepo = vi.hoisted(() => vi.fn());
const hoistedMockInit = vi.hoisted(() => vi.fn());
const hoistedMockRaw = vi.hoisted(() => vi.fn());
const hoistedMockAdd = vi.hoisted(() => vi.fn());
const hoistedMockCommit = vi.hoisted(() => vi.fn());
const hoistedMockStatus = vi.hoisted(() => vi.fn());
vi.mock('simple-git', () => ({
    simpleGit: hoistedMockSimpleGit.mockImplementation(() => ({
        checkIsRepo: hoistedMockCheckIsRepo,
        init: hoistedMockInit,
        raw: hoistedMockRaw,
        add: hoistedMockAdd,
        commit: hoistedMockCommit,
        status: hoistedMockStatus,
        env: hoistedMockEnv,
    })),
    CheckRepoActions: { IS_REPO_ROOT: 'is-repo-root' },
}));
const hoistedIsGitRepositoryMock = vi.hoisted(() => vi.fn());
vi.mock('../utils/gitUtils.js', () => ({
    isGitRepository: hoistedIsGitRepositoryMock,
}));
const hoistedMockHomedir = vi.hoisted(() => vi.fn());
vi.mock('os', async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        homedir: hoistedMockHomedir,
    };
});
const hoistedMockDebugLogger = vi.hoisted(() => ({
    debug: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
}));
vi.mock('../utils/debugLogger.js', () => ({
    debugLogger: hoistedMockDebugLogger,
}));
describe('GitService', () => {
    let testRootDir;
    let projectRoot;
    let homedir;
    let hash;
    let storage;
    beforeEach(async () => {
        testRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'git-service-test-'));
        projectRoot = path.join(testRootDir, 'project');
        homedir = path.join(testRootDir, 'home');
        await fs.mkdir(projectRoot, { recursive: true });
        await fs.mkdir(homedir, { recursive: true });
        hash = getProjectHash(projectRoot);
        vi.clearAllMocks();
        hoistedIsGitRepositoryMock.mockReturnValue(true);
        spawnAsync.mockResolvedValue({
            stdout: 'git version 2.0.0',
            stderr: '',
        });
        hoistedMockHomedir.mockReturnValue(homedir);
        hoistedMockEnv.mockImplementation(() => ({
            checkIsRepo: hoistedMockCheckIsRepo,
            init: hoistedMockInit,
            raw: hoistedMockRaw,
            add: hoistedMockAdd,
            commit: hoistedMockCommit,
            status: hoistedMockStatus,
        }));
        hoistedMockSimpleGit.mockImplementation(() => ({
            checkIsRepo: hoistedMockCheckIsRepo,
            init: hoistedMockInit,
            raw: hoistedMockRaw,
            add: hoistedMockAdd,
            commit: hoistedMockCommit,
            status: hoistedMockStatus,
            env: hoistedMockEnv,
        }));
        hoistedMockCheckIsRepo.mockResolvedValue(false);
        hoistedMockInit.mockResolvedValue(undefined);
        hoistedMockRaw.mockResolvedValue('');
        hoistedMockAdd.mockResolvedValue(undefined);
        hoistedMockCommit.mockResolvedValue({
            commit: 'initial',
        });
        storage = new Storage(projectRoot);
    });
    afterEach(async () => {
        vi.restoreAllMocks();
        await fs.rm(testRootDir, { recursive: true, force: true });
    });
    describe('constructor', () => {
        it('should successfully create an instance', () => {
            expect(() => new GitService(projectRoot, storage)).not.toThrow();
        });
    });
    describe('verifyGitAvailability', () => {
        it('should resolve true if git --version command succeeds', async () => {
            const service = new GitService(projectRoot, storage);
            await expect(service.verifyGitAvailability()).resolves.toBe(true);
            expect(spawnAsync).toHaveBeenCalledWith('git', ['--version']);
        });
        it('should resolve false if git --version command fails', async () => {
            spawnAsync.mockRejectedValue(new Error('git not found'));
            const service = new GitService(projectRoot, storage);
            await expect(service.verifyGitAvailability()).resolves.toBe(false);
        });
    });
    describe('initialize', () => {
        it('should throw an error if Git is not available', async () => {
            spawnAsync.mockRejectedValue(new Error('git not found'));
            const service = new GitService(projectRoot, storage);
            await expect(service.initialize()).rejects.toThrow('Checkpointing is enabled, but Git is not installed. Please install Git or disable checkpointing to continue.');
        });
        it('should call setupShadowGitRepository if Git is available', async () => {
            const service = new GitService(projectRoot, storage);
            const setupSpy = vi
                .spyOn(service, 'setupShadowGitRepository')
                .mockResolvedValue(undefined);
            await service.initialize();
            expect(setupSpy).toHaveBeenCalled();
        });
    });
    describe('setupShadowGitRepository', () => {
        let repoDir;
        let gitConfigPath;
        beforeEach(() => {
            repoDir = path.join(homedir, GEMINI_DIR, 'history', hash);
            gitConfigPath = path.join(repoDir, '.gitconfig');
        });
        it('should create history and repository directories', async () => {
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            const stats = await fs.stat(repoDir);
            expect(stats.isDirectory()).toBe(true);
        });
        it('should create a .gitconfig file with the correct content', async () => {
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            const expectedConfigContent = '[user]\n  name = Gemini CLI\n  email = gemini-cli@google.com\n[commit]\n  gpgsign = false\n';
            const actualConfigContent = await fs.readFile(gitConfigPath, 'utf-8');
            expect(actualConfigContent).toBe(expectedConfigContent);
        });
        it('should initialize git repo in historyDir if not already initialized', async () => {
            hoistedMockCheckIsRepo.mockResolvedValue(false);
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            expect(hoistedMockSimpleGit).toHaveBeenCalledWith(repoDir);
            expect(hoistedMockInit).toHaveBeenCalled();
        });
        it('should not initialize git repo if already initialized', async () => {
            hoistedMockCheckIsRepo.mockResolvedValue(true);
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            expect(hoistedMockInit).not.toHaveBeenCalled();
        });
        it('should copy .gitignore from projectRoot if it exists', async () => {
            const gitignoreContent = 'node_modules/\n.env';
            const visibleGitIgnorePath = path.join(projectRoot, '.gitignore');
            await fs.writeFile(visibleGitIgnorePath, gitignoreContent);
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
            const copiedContent = await fs.readFile(hiddenGitIgnorePath, 'utf-8');
            expect(copiedContent).toBe(gitignoreContent);
        });
        it('should not create a .gitignore in shadow repo if project .gitignore does not exist', async () => {
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
            // An empty string is written if the file doesn't exist.
            const content = await fs.readFile(hiddenGitIgnorePath, 'utf-8');
            expect(content).toBe('');
        });
        it('should throw an error if reading projectRoot .gitignore fails with other errors', async () => {
            const visibleGitIgnorePath = path.join(projectRoot, '.gitignore');
            // Create a directory instead of a file to cause a read error
            await fs.mkdir(visibleGitIgnorePath);
            const service = new GitService(projectRoot, storage);
            // EISDIR is the expected error code on Unix-like systems
            await expect(service.setupShadowGitRepository()).rejects.toThrow(/EISDIR: illegal operation on a directory, read|EBUSY: resource busy or locked, read/);
        });
        it('should make an initial commit if no commits exist in history repo', async () => {
            hoistedMockCheckIsRepo.mockResolvedValue(false);
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            expect(hoistedMockCommit).toHaveBeenCalledWith('Initial commit', {
                '--allow-empty': null,
            });
        });
        it('should not make an initial commit if commits already exist', async () => {
            hoistedMockCheckIsRepo.mockResolvedValue(true);
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            expect(hoistedMockCommit).not.toHaveBeenCalled();
        });
        it('should handle checkIsRepo failure gracefully and initialize repo', async () => {
            // Simulate checkIsRepo failing (e.g., on certain Git versions like macOS 2.39.5)
            hoistedMockCheckIsRepo.mockRejectedValue(new Error('git rev-parse --is-inside-work-tree failed'));
            const service = new GitService(projectRoot, storage);
            await service.setupShadowGitRepository();
            // Should proceed to initialize the repo since checkIsRepo failed
            expect(hoistedMockInit).toHaveBeenCalled();
            // Should log the error using debugLogger
            expect(hoistedMockDebugLogger.debug).toHaveBeenCalledWith(expect.stringContaining('checkIsRepo failed'));
        });
    });
    describe('createFileSnapshot', () => {
        it('should commit with --no-verify flag', async () => {
            hoistedMockStatus.mockResolvedValue({ isClean: () => false });
            const service = new GitService(projectRoot, storage);
            await service.initialize();
            await service.createFileSnapshot('test commit');
            expect(hoistedMockCommit).toHaveBeenCalledWith('test commit', {
                '--no-verify': null,
            });
        });
        it('should create a new commit if there are staged changes', async () => {
            hoistedMockStatus.mockResolvedValue({ isClean: () => false });
            hoistedMockCommit.mockResolvedValue({ commit: 'new-commit-hash' });
            const service = new GitService(projectRoot, storage);
            const commitHash = await service.createFileSnapshot('test message');
            expect(hoistedMockAdd).toHaveBeenCalledWith('.');
            expect(hoistedMockStatus).toHaveBeenCalled();
            expect(hoistedMockCommit).toHaveBeenCalledWith('test message', {
                '--no-verify': null,
            });
            expect(commitHash).toBe('new-commit-hash');
        });
        it('should return the current HEAD commit hash if there are no staged changes', async () => {
            hoistedMockStatus.mockResolvedValue({ isClean: () => true });
            hoistedMockRaw.mockResolvedValue('current-head-hash');
            const service = new GitService(projectRoot, storage);
            const commitHash = await service.createFileSnapshot('test message');
            expect(hoistedMockAdd).toHaveBeenCalledWith('.');
            expect(hoistedMockStatus).toHaveBeenCalled();
            expect(hoistedMockCommit).not.toHaveBeenCalled();
            expect(hoistedMockRaw).toHaveBeenCalledWith('rev-parse', 'HEAD');
            expect(commitHash).toBe('current-head-hash');
        });
    });
});
//# sourceMappingURL=gitService.test.js.map