import { ConfigLoader } from '../core/impl/config-loader.impl';
import fs from 'fs/promises';
import path from 'path';

// Mock fs/promises
jest.mock('fs/promises', () => ({
    access: jest.fn().mockResolvedValue(undefined),
    readFile: jest.fn()
}));

// Mock path
jest.mock('path', () => ({
    join: jest.fn((base, ...parts) => `${base}/${parts.join('/')}`),
    isAbsolute: jest.fn(p => p.startsWith('/')),
}));

describe('ConfigLoader', () => {
    let configLoader: ConfigLoader;
    const baseDir = '/test/base/dir';

    beforeEach(() => {
        jest.clearAllMocks();
        // Reset mock implementations to defaults
        (path.join as jest.Mock).mockImplementation((base, ...parts) => `${base}/${parts.join('/')}`);
        (path.isAbsolute as jest.Mock).mockImplementation(p => p.startsWith('/'));
        (fs.access as jest.Mock).mockResolvedValue(undefined);

        // Create a fresh instance for each test
        configLoader = new ConfigLoader(baseDir);
    });

    describe('loadConfig', () => {
        it('should load JSON config file', async () => {
            // Setup
            const configPath = 'test-config.json';
            const resolvedPath = `${baseDir}/${configPath}`;
            const configContent = JSON.stringify({
                skipTransform: false,
                transformations: {
                    mappings: [{ source: '$.spec.value', target: '$.Test.value' }],
                    replacements: []
                }
            });

            (fs.readFile as jest.Mock).mockResolvedValue(configContent);
            (path.join as jest.Mock).mockReturnValue(resolvedPath);

            // Execute
            const result = await configLoader.loadConfig(configPath);

            // Verify
            expect(fs.access).toHaveBeenCalledWith(resolvedPath);
            expect(fs.readFile).toHaveBeenCalledWith(resolvedPath, 'utf-8');
            expect(result).toEqual({
                skipTransform: false,
                transformations: {
                    mappings: [{ source: '$.spec.value', target: '$.Test.value' }],
                    replacements: []
                }
            });
        });

        it('should handle config with inheritance', async () => {
            // Setup
            const configPath = 'child-config.json';
            const parentPath = 'parent-config.json';
            const resolvedConfigPath = `${baseDir}/${configPath}`;
            const resolvedParentPath = `${baseDir}/${parentPath}`;

            const parentConfig = {
                skipTransform: false,
                transformations: {
                    mappings: [{ source: '$.spec.parentValue', target: '$.Test.parentValue' }],
                    replacements: []
                }
            };

            const childConfig = {
                extends: parentPath,
                transformations: {
                    mappings: [{ source: '$.spec.childValue', target: '$.Test.childValue' }],
                    replacements: [{ target: '$.Test.replacement', value: 'test', precedence: 10, operation: 'replace' }]
                }
            };

            (fs.readFile as jest.Mock).mockImplementation((path) => {
                if (path === resolvedConfigPath) {
                    return Promise.resolve(JSON.stringify(childConfig));
                } else if (path === resolvedParentPath) {
                    return Promise.resolve(JSON.stringify(parentConfig));
                }
                return Promise.reject(new Error(`Unexpected path: ${path}`));
            });

            (path.join as jest.Mock).mockImplementation((base, file) => {
                if (file === configPath) return resolvedConfigPath;
                if (file === parentPath) return resolvedParentPath;
                return `${base}/${file}`;
            });

            // Execute
            const result = await configLoader.loadConfig(configPath);

            // Verify
            expect(fs.access).toHaveBeenCalledWith(resolvedConfigPath);
            expect(fs.access).toHaveBeenCalledWith(resolvedParentPath);
            expect(fs.readFile).toHaveBeenCalledWith(resolvedConfigPath, 'utf-8');
            expect(fs.readFile).toHaveBeenCalledWith(resolvedParentPath, 'utf-8');

            // The actual implementation might not set skipTransform if it's not in the config
            expect(result).toHaveProperty('extends', parentPath);
            expect(result.transformations).toBeDefined();
            if (result.transformations) {
                expect(result.transformations.mappings).toContainEqual({ source: '$.spec.parentValue', target: '$.Test.parentValue' });
                expect(result.transformations.mappings).toContainEqual({ source: '$.spec.childValue', target: '$.Test.childValue' });
                expect(result.transformations.replacements).toContainEqual({ target: '$.Test.replacement', value: 'test', precedence: 10, operation: 'replace' });
            }
        });

        it('should handle config with custom transformer', async () => {
            // Setup
            const configPath = 'custom-config.json';
            const resolvedPath = `${baseDir}/${configPath}`;
            const configContent = JSON.stringify({
                skipTransform: false,
                custom: 'custom-transformer',
                transformations: {
                    mappings: [{ source: '$.spec.value', target: '$.Test.value' }],
                    replacements: []
                }
            });

            (fs.readFile as jest.Mock).mockResolvedValue(configContent);
            (path.join as jest.Mock).mockReturnValue(resolvedPath);

            // Execute
            const result = await configLoader.loadConfig(configPath);

            // Verify
            expect(result).toEqual({
                skipTransform: false,
                custom: 'custom-transformer'
                // transformations should be removed when custom is specified
            });
        });

        it('should handle config with inheritance and custom transformer', async () => {
            // Setup
            const configPath = 'child-config.json';
            const parentPath = 'parent-config.json';
            const resolvedConfigPath = `${baseDir}/${configPath}`;
            const resolvedParentPath = `${baseDir}/${parentPath}`;

            const parentConfig = {
                skipTransform: false,
                transformations: {
                    mappings: [{ source: '$.spec.parentValue', target: '$.Test.parentValue' }],
                    replacements: []
                }
            };

            const childConfig = {
                extends: parentPath,
                custom: 'custom-transformer',
                transformations: {
                    mappings: [{ source: '$.spec.childValue', target: '$.Test.childValue' }],
                    replacements: []
                }
            };

            (fs.readFile as jest.Mock).mockImplementation((path) => {
                if (path === resolvedConfigPath) {
                    return Promise.resolve(JSON.stringify(childConfig));
                } else if (path === resolvedParentPath) {
                    return Promise.resolve(JSON.stringify(parentConfig));
                }
                return Promise.reject(new Error(`Unexpected path: ${path}`));
            });

            (path.join as jest.Mock).mockImplementation((base, file) => {
                if (file === configPath) return resolvedConfigPath;
                if (file === parentPath) return resolvedParentPath;
                return `${base}/${file}`;
            });

            // Execute
            const result = await configLoader.loadConfig(configPath);

            // Verify
            // The actual implementation might not set skipTransform if it's not in the config
            expect(result).toHaveProperty('extends', parentPath);
            expect(result).toHaveProperty('custom', 'custom-transformer');
            // transformations should be removed when custom is specified
            expect(result.transformations).toBeUndefined();
        });

        it('should cache loaded configs', async () => {
            // Setup
            const configPath = 'test-config.json';
            const resolvedPath = `${baseDir}/${configPath}`;
            const configContent = JSON.stringify({
                skipTransform: false,
                transformations: {
                    mappings: [{ source: '$.spec.value', target: '$.Test.value' }],
                    replacements: []
                }
            });

            (fs.readFile as jest.Mock).mockResolvedValue(configContent);
            (path.join as jest.Mock).mockReturnValue(resolvedPath);

            // Execute
            await configLoader.loadConfig(configPath);
            await configLoader.loadConfig(configPath);

            // Verify
            expect(fs.readFile).toHaveBeenCalledTimes(1);
        });

        it('should handle errors when loading config', async () => {
            // Setup
            const configPath = 'nonexistent-config.json';
            const resolvedPath = `${baseDir}/${configPath}`;

            (fs.access as jest.Mock).mockRejectedValue(new Error('File not found'));
            (path.join as jest.Mock).mockReturnValue(resolvedPath);

            // Execute & Verify
            await expect(configLoader.loadConfig(configPath)).rejects.toThrow();
        });
    });

    describe('resolveConfigPath', () => {
        it('should resolve paths starting with src/', async () => {
            // Setup
            const configPath = 'src/configs/test-config.json';
            const expectedPath = `${baseDir}/configs/test-config.json`;
            (path.join as jest.Mock).mockReturnValueOnce(expectedPath);

            // Execute
            const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);

            // Verify
            expect(resolvedPath).toBe(expectedPath);
            expect(path.join).toHaveBeenCalledWith(baseDir, 'configs/test-config.json');
        });

        it('should resolve paths starting with /src/', async () => {
            // Setup
            const configPath = '/src/configs/test-config.json';
            const expectedPath = `${baseDir}/configs/test-config.json`;
            (path.join as jest.Mock).mockReturnValue(expectedPath);

            // Execute
            const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);

            // Verify
            expect(resolvedPath).toBe(expectedPath);
            expect(path.join).toHaveBeenCalledWith(baseDir, 'configs/test-config.json');
        });

        it('should resolve relative paths', async () => {
            // Setup
            const configPath = 'configs/test-config.json';
            const expectedPath = `${baseDir}/configs/test-config.json`;
            (path.join as jest.Mock).mockReturnValue(expectedPath);
            (path.isAbsolute as jest.Mock).mockReturnValue(false);

            // Execute
            const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);

            // Verify
            expect(resolvedPath).toBe(expectedPath);
            expect(path.join).toHaveBeenCalledWith(baseDir, configPath);
        });

        it('should resolve absolute paths containing /src/', async () => {
            // Setup
            const configPath = '/absolute/path/src/configs/test-config.json';
            const expectedPath = `${baseDir}/configs/test-config.json`;
            (path.isAbsolute as jest.Mock).mockReturnValue(true);
            (path.join as jest.Mock).mockReturnValue(expectedPath);

            // Execute
            const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);

            // Verify
            expect(resolvedPath).toBe(expectedPath);
        });

        it('should handle other absolute paths', async () => {
            // Setup
            const configPath = '/absolute/path/configs/test-config.json';
            (path.isAbsolute as jest.Mock).mockReturnValue(true);

            // Execute
            const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);

            // Verify
            expect(resolvedPath).toBe(configPath);
        });

        it('should throw error if file does not exist', async () => {
            // Setup
            const configPath = 'nonexistent-config.json';
            const expectedPath = `${baseDir}/nonexistent-config.json`;
            (path.join as jest.Mock).mockReturnValue(expectedPath);
            (fs.access as jest.Mock).mockRejectedValue(new Error('File not found'));

            // Execute & Verify
            await expect((configLoader as any).resolveConfigPath(configPath)).rejects.toThrow();
        });
    });
});

