/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { Storage } from '@google-cloud/storage';
import * as fse from 'fs-extra';
import * as tar from 'tar';
import { gzipSync, gunzipSync } from 'node:zlib';
import { v4 as uuidv4 } from 'uuid';
import { describe, it, expect, beforeEach, vi, } from 'vitest';
import { GCSTaskStore, NoOpTaskStore } from './gcs.js';
import { logger } from '../utils/logger.js';
import * as configModule from '../config/config.js';
import { getPersistedState, METADATA_KEY } from '../types.js';
// Mock dependencies
const fsMocks = vi.hoisted(() => ({
    readdir: vi.fn(),
    createReadStream: vi.fn(),
}));
vi.mock('@google-cloud/storage');
vi.mock('fs-extra', () => ({
    pathExists: vi.fn(),
    readdir: vi.fn(),
    remove: vi.fn(),
    ensureDir: vi.fn(),
    createReadStream: vi.fn(),
}));
vi.mock('node:fs', async () => {
    const actual = await vi.importActual('node:fs');
    return {
        ...actual,
        promises: {
            ...actual.promises,
            readdir: fsMocks.readdir,
        },
        createReadStream: fsMocks.createReadStream,
    };
});
vi.mock('fs', async () => {
    const actual = await vi.importActual('node:fs');
    return {
        ...actual,
        promises: {
            ...actual.promises,
            readdir: fsMocks.readdir,
        },
        createReadStream: fsMocks.createReadStream,
    };
});
vi.mock('tar', async () => {
    const actualFs = await vi.importActual('node:fs');
    return {
        c: vi.fn(({ file }) => {
            if (file) {
                actualFs.writeFileSync(file, Buffer.from('dummy tar content'));
            }
            return Promise.resolve();
        }),
        x: vi.fn().mockResolvedValue(undefined),
        t: vi.fn().mockResolvedValue(undefined),
        r: vi.fn().mockResolvedValue(undefined),
        u: vi.fn().mockResolvedValue(undefined),
    };
});
vi.mock('zlib');
vi.mock('uuid');
vi.mock('../utils/logger.js', () => ({
    logger: {
        info: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
        debug: vi.fn(),
    },
}));
vi.mock('../config/config.js', () => ({
    setTargetDir: vi.fn(),
}));
vi.mock('node:stream/promises', () => ({
    pipeline: vi.fn(),
}));
vi.mock('../types.js', async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        getPersistedState: vi.fn(),
    };
});
const mockStorage = Storage;
const mockFse = fse;
const mockCreateReadStream = fsMocks.createReadStream;
const mockTar = tar;
const mockGzipSync = gzipSync;
const mockGunzipSync = gunzipSync;
const mockUuidv4 = uuidv4;
const mockSetTargetDir = configModule.setTargetDir;
const mockGetPersistedState = getPersistedState;
const TEST_METADATA_KEY = METADATA_KEY || '__persistedState';
describe('GCSTaskStore', () => {
    let bucketName;
    let mockBucket;
    let mockFile;
    let mockWriteStream;
    let mockStorageInstance;
    beforeEach(() => {
        vi.clearAllMocks();
        bucketName = 'test-bucket';
        mockWriteStream = {
            emit: vi.fn().mockReturnValue(true),
            removeListener: vi.fn().mockReturnValue(mockWriteStream),
            on: vi.fn((event, cb) => {
                if (event === 'finish')
                    setTimeout(cb, 0); // Simulate async finish
                return mockWriteStream;
            }),
            once: vi.fn((event, cb) => {
                if (event === 'finish')
                    setTimeout(cb, 0); // Simulate async finish        return mockWriteStream;
            }),
            destroy: vi.fn(),
            write: vi.fn().mockReturnValue(true),
            end: vi.fn(),
            destroyed: false,
        };
        mockFile = {
            save: vi.fn().mockResolvedValue(undefined),
            download: vi.fn().mockResolvedValue([Buffer.from('')]),
            exists: vi.fn().mockResolvedValue([true]),
            createWriteStream: vi.fn().mockReturnValue(mockWriteStream),
        };
        mockBucket = {
            exists: vi.fn().mockResolvedValue([true]),
            file: vi.fn().mockReturnValue(mockFile),
            name: bucketName,
        };
        mockStorageInstance = {
            bucket: vi.fn().mockReturnValue(mockBucket),
            getBuckets: vi.fn().mockResolvedValue([[{ name: bucketName }]]),
            createBucket: vi.fn().mockResolvedValue([mockBucket]),
        };
        mockStorage.mockReturnValue(mockStorageInstance);
        mockUuidv4.mockReturnValue('test-uuid');
        mockSetTargetDir.mockReturnValue('/tmp/workdir');
        mockGetPersistedState.mockReturnValue({
            _agentSettings: {},
            _taskState: 'submitted',
        });
        fse.pathExists.mockResolvedValue(true);
        fsMocks.readdir.mockResolvedValue(['file1.txt']);
        mockFse.remove.mockResolvedValue(undefined);
        mockFse.ensureDir.mockResolvedValue(undefined);
        mockGzipSync.mockReturnValue(Buffer.from('compressed'));
        mockGunzipSync.mockReturnValue(Buffer.from('{}'));
        mockCreateReadStream.mockReturnValue({ on: vi.fn(), pipe: vi.fn() });
        mockFse.createReadStream.mockReturnValue({
            on: vi.fn(),
            pipe: vi.fn(),
        });
    });
    describe('Constructor & Initialization', () => {
        it('should initialize and check bucket existence', async () => {
            const store = new GCSTaskStore(bucketName);
            await store['ensureBucketInitialized']();
            expect(mockStorage).toHaveBeenCalledTimes(1);
            expect(mockStorageInstance.getBuckets).toHaveBeenCalled();
            expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Bucket test-bucket exists'));
        });
        it('should create bucket if it does not exist', async () => {
            mockStorageInstance.getBuckets.mockResolvedValue([[]]);
            const store = new GCSTaskStore(bucketName);
            await store['ensureBucketInitialized']();
            expect(mockStorageInstance.createBucket).toHaveBeenCalledWith(bucketName);
            expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Bucket test-bucket created successfully'));
        });
        it('should throw if bucket creation fails', async () => {
            mockStorageInstance.getBuckets.mockResolvedValue([[]]);
            mockStorageInstance.createBucket.mockRejectedValue(new Error('Create failed'));
            const store = new GCSTaskStore(bucketName);
            await expect(store['ensureBucketInitialized']()).rejects.toThrow('Failed to create GCS bucket test-bucket: Error: Create failed');
        });
    });
    describe('save', () => {
        const mockTask = {
            id: 'task1',
            contextId: 'ctx1',
            kind: 'task',
            status: { state: 'working' },
            metadata: {},
        };
        it('should save metadata and workspace', async () => {
            const store = new GCSTaskStore(bucketName);
            await store.save(mockTask);
            expect(mockFile.save).toHaveBeenCalledTimes(1);
            expect(mockTar.c).toHaveBeenCalledTimes(1);
            expect(mockFse.remove).toHaveBeenCalledTimes(1);
            expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('metadata saved to GCS'));
            expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('workspace saved to GCS'));
        });
        it('should handle tar creation failure', async () => {
            mockFse.pathExists.mockImplementation(async (path) => !path.toString().includes('task-task1-workspace-test-uuid.tar.gz'));
            const store = new GCSTaskStore(bucketName);
            await expect(store.save(mockTask)).rejects.toThrow('tar.c command failed to create');
        });
        it('should throw an error if taskId contains path traversal sequences', async () => {
            const store = new GCSTaskStore('test-bucket');
            const maliciousTask = {
                id: '../../../malicious-task',
                metadata: {
                    _internal: {
                        agentSettings: {
                            cacheDir: '/tmp/cache',
                            dataDir: '/tmp/data',
                            logDir: '/tmp/logs',
                            tempDir: '/tmp/temp',
                        },
                        taskState: 'working',
                    },
                },
                kind: 'task',
                status: {
                    state: 'working',
                    timestamp: new Date().toISOString(),
                },
                contextId: 'test-context',
                history: [],
                artifacts: [],
            };
            await expect(store.save(maliciousTask)).rejects.toThrow('Invalid taskId: ../../../malicious-task');
        });
    });
    describe('load', () => {
        it('should load task metadata and workspace', async () => {
            mockGunzipSync.mockReturnValue(Buffer.from(JSON.stringify({
                [TEST_METADATA_KEY]: {
                    _agentSettings: {},
                    _taskState: 'submitted',
                },
                _contextId: 'ctx1',
            })));
            mockFile.download.mockResolvedValue([Buffer.from('compressed metadata')]);
            mockFile.download.mockResolvedValueOnce([
                Buffer.from('compressed metadata'),
            ]);
            mockBucket.file = vi.fn((path) => {
                const newMockFile = { ...mockFile };
                if (path.includes('metadata')) {
                    newMockFile.download = vi
                        .fn()
                        .mockResolvedValue([Buffer.from('compressed metadata')]);
                    newMockFile.exists = vi.fn().mockResolvedValue([true]);
                }
                else {
                    newMockFile.download = vi
                        .fn()
                        .mockResolvedValue([Buffer.from('compressed workspace')]);
                    newMockFile.exists = vi.fn().mockResolvedValue([true]);
                }
                return newMockFile;
            });
            const store = new GCSTaskStore(bucketName);
            const task = await store.load('task1');
            expect(task).toBeDefined();
            expect(task?.id).toBe('task1');
            expect(mockBucket.file).toHaveBeenCalledWith('tasks/task1/metadata.tar.gz');
            expect(mockBucket.file).toHaveBeenCalledWith('tasks/task1/workspace.tar.gz');
            expect(mockTar.x).toHaveBeenCalledTimes(1);
            expect(mockFse.remove).toHaveBeenCalledTimes(1);
        });
        it('should return undefined if metadata not found', async () => {
            mockFile.exists.mockResolvedValue([false]);
            const store = new GCSTaskStore(bucketName);
            const task = await store.load('task1');
            expect(task).toBeUndefined();
            expect(mockBucket.file).toHaveBeenCalledWith('tasks/task1/metadata.tar.gz');
        });
        it('should load metadata even if workspace not found', async () => {
            mockGunzipSync.mockReturnValue(Buffer.from(JSON.stringify({
                [TEST_METADATA_KEY]: {
                    _agentSettings: {},
                    _taskState: 'submitted',
                },
                _contextId: 'ctx1',
            })));
            mockBucket.file = vi.fn((path) => {
                const newMockFile = { ...mockFile };
                if (path.includes('workspace.tar.gz')) {
                    newMockFile.exists = vi.fn().mockResolvedValue([false]);
                }
                else {
                    newMockFile.exists = vi.fn().mockResolvedValue([true]);
                    newMockFile.download = vi
                        .fn()
                        .mockResolvedValue([Buffer.from('compressed metadata')]);
                }
                return newMockFile;
            });
            const store = new GCSTaskStore(bucketName);
            const task = await store.load('task1');
            expect(task).toBeDefined();
            expect(mockTar.x).not.toHaveBeenCalled();
            expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('workspace archive not found'));
        });
    });
    it('should throw an error if taskId contains path traversal sequences', async () => {
        const store = new GCSTaskStore('test-bucket');
        const maliciousTaskId = '../../../malicious-task';
        await expect(store.load(maliciousTaskId)).rejects.toThrow(`Invalid taskId: ${maliciousTaskId}`);
    });
});
describe('NoOpTaskStore', () => {
    let realStore;
    let noOpStore;
    beforeEach(() => {
        // Create a mock of the real store to delegate to
        realStore = {
            save: vi.fn(),
            load: vi.fn().mockResolvedValue({ id: 'task-123' }),
        };
        noOpStore = new NoOpTaskStore(realStore);
    });
    it("should not call the real store's save method", async () => {
        const mockTask = { id: 'test-task' };
        await noOpStore.save(mockTask);
        expect(realStore.save).not.toHaveBeenCalled();
    });
    it('should delegate the load method to the real store', async () => {
        const taskId = 'task-123';
        const result = await noOpStore.load(taskId);
        expect(realStore.load).toHaveBeenCalledWith(taskId);
        expect(result).toBeDefined();
        expect(result?.id).toBe(taskId);
    });
});
//# sourceMappingURL=gcs.test.js.map