/**
* Copyright Super iPaaS Integration LLC, an IBM Company 2024
*/

import AdmZip from 'adm-zip';
import fs from 'node:fs';
import { bundleApiDependency, addApiDependencyAsset, resolveAndAddApiSpec } from './api-build-helper.js';
import { AssetCacheModel } from '../../model/asset-cache-model.js';
import { BaseAsset } from '../../model/assets-model.js';
import * as messageHelper from '../common/message-helper.js';

// Mock dependencies
jest.mock('adm-zip');
jest.mock('../common/message-helper.js', () => ({
    showInfo: jest.fn(),
    showWarning: jest.fn(),
    showError: jest.fn(),
}));

describe('API Build Helper Test Suite', () => {
    let mockZip: jest.Mocked<AdmZip>;
    let existsSyncSpy: jest.SpyInstance;

    beforeEach(() => {
        jest.clearAllMocks();
        mockZip = {
            addLocalFile: jest.fn(),
        } as any;
        existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true);
    });

    afterEach(() => {
        existsSyncSpy.mockRestore();
    });

    describe('addApiDependencyAsset', () => {
        it('should add API dependency with correct nested folder structure', () => {
            const mockDirent = {
                name: 'payment-api.yml',
                parentPath: '/root/ProjectB/api-assets',
                path: '/root/ProjectB/api-assets',
                isDirectory: () => false,
                isFile: () => true,
                isBlockDevice: () => false,
                isCharacterDevice: () => false,
                isFIFO: () => false,
                isSocket: () => false,
                isSymbolicLink: () => false,
                [Symbol.toStringTag]: 'Dirent',
            } as unknown as fs.Dirent;

            const result = addApiDependencyAsset(
                mockDirent,
                mockZip,
                'ProjectB',
                'ProjectA',
                '/root',
                0
            );

            expect(result).toBe('api-assets/payment-api.yml');
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/api-assets/payment-api.yml',
                'ProjectA/ProjectB_0/api-assets'
            );
        });

        it('should handle API in nested subdirectories', () => {
            const mockDirent = {
                name: 'api.yml',
                parentPath: '/root/ProjectB/apis/v1/payment',
                path: '/root/ProjectB/apis/v1/payment',
                isDirectory: () => false,
                isFile: () => true,
                isBlockDevice: () => false,
                isCharacterDevice: () => false,
                isFIFO: () => false,
                isSocket: () => false,
                isSymbolicLink: () => false,
                [Symbol.toStringTag]: 'Dirent',
            } as unknown as fs.Dirent;

            const result = addApiDependencyAsset(
                mockDirent,
                mockZip,
                'ProjectB',
                'ProjectA',
                '/root',
                0
            );

            expect(result).toBe('apis/v1/payment/api.yml');
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/apis/v1/payment/api.yml',
                'ProjectA/ProjectB_0/apis/v1/payment'
            );
        });

        it('should handle API in project root directory', () => {
            const mockDirent = {
                name: 'root-api.yml',
                parentPath: '/root/ProjectB',
                path: '/root/ProjectB',
                isDirectory: () => false,
                isFile: () => true,
                isBlockDevice: () => false,
                isCharacterDevice: () => false,
                isFIFO: () => false,
                isSocket: () => false,
                isSymbolicLink: () => false,
                [Symbol.toStringTag]: 'Dirent',
            } as unknown as fs.Dirent;

            const result = addApiDependencyAsset(
                mockDirent,
                mockZip,
                'ProjectB',
                'ProjectA',
                '/root',
                1
            );

            expect(result).toBe('root-api.yml');
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/root-api.yml',
                'ProjectA/ProjectB_1'
            );
        });
    });

    describe('resolveAndAddApiSpec', () => {
        const mockApiAsset = {
            kind: 'api',
            apiVersion: 'api.webmethods.io/v1',
            metadata: {
                name: 'PaymentAPI',
                namespace: 'dev',
                version: '1.0',
            },
            spec: {
                'api-spec': {
                    $path: '../specs/petstore.yaml',
                },
            },
        } as unknown as BaseAsset;

        beforeEach(() => {
            existsSyncSpy.mockClear();
            existsSyncSpy.mockReturnValue(true);
        });

        it('should resolve and add spec with relative path using ../', () => {
            resolveAndAddApiSpec(
                mockApiAsset,
                mockZip,
                'api-assets/payment-api.yml',
                'ProjectB',
                'ProjectA',
                '/root',
                0
            );

            expect(fs.existsSync).toHaveBeenCalledWith('/root/ProjectB/specs/petstore.yaml');
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/specs/petstore.yaml',
                'ProjectA/ProjectB_0/specs'
            );
            expect(messageHelper.showInfo).toHaveBeenCalledWith(
                'Spec added: ProjectA/ProjectB_0/specs/petstore.yaml'
            );
        });

        it('should resolve and add spec with path relative to project root', () => {
            const apiAssetProjectRoot = {
                ...mockApiAsset,
                spec: {
                    'api-spec': {
                        $path: 'specs/petstore.yaml',
                    },
                },
            } as unknown as BaseAsset;

            resolveAndAddApiSpec(
                apiAssetProjectRoot,
                mockZip,
                'api-assets/payment-api.yml',
                'ProjectB',
                'ProjectA',
                '/root',
                0
            );

            // Without ../ or ./ prefix, path is relative to project root
            expect(fs.existsSync).toHaveBeenCalledWith('/root/ProjectB/specs/petstore.yaml');
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/specs/petstore.yaml',
                'ProjectA/ProjectB_0/specs'
            );
        });

        it('should resolve and add spec with multiple ../ in path', () => {
            const apiAssetMultipleUp = {
                ...mockApiAsset,
                spec: {
                    'api-spec': {
                        $path: '../../common/specs/api.yaml',
                    },
                },
            } as unknown as BaseAsset;

            resolveAndAddApiSpec(
                apiAssetMultipleUp,
                mockZip,
                'apis/v1/payment/api.yml',
                'ProjectB',
                'ProjectA',
                '/root',
                0
            );

            // From apis/v1/payment, going up ../../ lands in apis/, then common/specs/api.yaml
            expect(fs.existsSync).toHaveBeenCalledWith('/root/ProjectB/apis/common/specs/api.yaml');
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/apis/common/specs/api.yaml',
                'ProjectA/ProjectB_0/apis/common/specs'
            );
        });

        it('should handle absolute path for spec', () => {
            const apiAssetAbsolute = {
                ...mockApiAsset,
                spec: {
                    'api-spec': {
                        $path: '/absolute/path/to/spec.yaml',
                    },
                },
            } as unknown as BaseAsset;

            resolveAndAddApiSpec(
                apiAssetAbsolute,
                mockZip,
                'api-assets/api.yml',
                'ProjectB',
                'ProjectA',
                '/root',
                0
            );

            expect(fs.existsSync).toHaveBeenCalledWith('/root/ProjectB/absolute/path/to/spec.yaml');
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/absolute/path/to/spec.yaml',
                'ProjectA/ProjectB_0/absolute/path/to'
            );
        });

        it('should show warning when spec file does not exist', () => {
            existsSyncSpy.mockReturnValue(false);

            resolveAndAddApiSpec(
                mockApiAsset,
                mockZip,
                'api-assets/payment-api.yml',
                'ProjectB',
                'ProjectA',
                '/root',
                0
            );

            expect(messageHelper.showWarning).toHaveBeenCalledWith(
                'API spec not found: /root/ProjectB/specs/petstore.yaml for API dev:PaymentAPI:1.0'
            );
            expect(mockZip.addLocalFile).not.toHaveBeenCalled();
        });

        it('should throw error when spec is not defined', () => {
            const apiAssetNoSpec = {
                kind: 'api',
                metadata: {
                    name: 'PaymentAPI',
                },
                spec: null,
            } as unknown as BaseAsset;

            expect(() => {
                resolveAndAddApiSpec(
                    apiAssetNoSpec,
                    mockZip,
                    'api-assets/api.yml',
                    'ProjectB',
                    'ProjectA',
                    '/root',
                    0
                );
            }).toThrow("Spec is not defined for the asset with kind 'API' and name 'PaymentAPI'");
        });

        it('should throw error when api-spec attribute is not defined', () => {
            const apiAssetNoApiSpec = {
                kind: 'api',
                metadata: {
                    name: 'PaymentAPI',
                },
                spec: {},
            } as unknown as BaseAsset;

            expect(() => {
                resolveAndAddApiSpec(
                    apiAssetNoApiSpec,
                    mockZip,
                    'api-assets/api.yml',
                    'ProjectB',
                    'ProjectA',
                    '/root',
                    0
                );
            }).toThrow("Attribute 'api-spec' is not defined");
        });

        it('should throw error when $path is not defined', () => {
            const apiAssetNoPath = {
                kind: 'api',
                metadata: {
                    name: 'PaymentAPI',
                },
                spec: {
                    'api-spec': {},
                },
            } as unknown as BaseAsset;

            expect(() => {
                resolveAndAddApiSpec(
                    apiAssetNoPath,
                    mockZip,
                    'api-assets/api.yml',
                    'ProjectB',
                    'ProjectA',
                    '/root',
                    0
                );
            }).toThrow('API Definition Path is not found');
        });

        it('should handle spec in nested subdirectories with ./', () => {
            const apiAssetDotSlash = {
                ...mockApiAsset,
                spec: {
                    'api-spec': {
                        $path: './specs/nested/api.yaml',
                    },
                },
            } as unknown as BaseAsset;

            resolveAndAddApiSpec(
                apiAssetDotSlash,
                mockZip,
                'api-assets/api.yml',
                'ProjectB',
                'ProjectA',
                '/root',
                0
            );

            expect(fs.existsSync).toHaveBeenCalledWith('/root/ProjectB/api-assets/specs/nested/api.yaml');
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/api-assets/specs/nested/api.yaml',
                'ProjectA/ProjectB_0/api-assets/specs/nested'
            );
        });
    });

    describe('bundleApiDependency', () => {
        const mockDirent = {
            name: 'payment-api.yml',
            parentPath: '/root/ProjectB/api-assets',
            path: '/root/ProjectB/api-assets',
            isDirectory: () => false,
            isFile: () => true,
            isBlockDevice: () => false,
            isCharacterDevice: () => false,
            isFIFO: () => false,
            isSocket: () => false,
            isSymbolicLink: () => false,
            [Symbol.toStringTag]: 'Dirent',
        } as unknown as fs.Dirent;

        const mockApiAsset = {
            kind: 'api',
            apiVersion: 'api.webmethods.io/v1',
            metadata: {
                name: 'PaymentAPI',
                namespace: 'dev',
                version: '1.0',
            },
            spec: {
                'api-spec': {
                    $path: '../specs/petstore.yaml',
                },
            },
        } as unknown as BaseAsset;

        const mockCachedAsset: AssetCacheModel = {
            kind: 'api',
            ref: 'dev:PaymentAPI:1.0',
            isNewlyAdded: false,
            sourceProject: 'ProjectB',
        };

        beforeEach(() => {
            existsSyncSpy.mockClear();
            existsSyncSpy.mockReturnValue(true);
            jest.spyOn(Date, 'now').mockReturnValue(0);
        });

        afterEach(() => {
            jest.restoreAllMocks();
        });

        it('should bundle API dependency with spec successfully', () => {
            bundleApiDependency(
                mockApiAsset,
                mockDirent,
                mockCachedAsset,
                '/root',
                'ProjectB',  // target project where API is located
                mockZip
            );

            // Verify API was added
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/api-assets/payment-api.yml',
                'ProjectB/ProjectB_0/api-assets'
            );

            // Verify spec was added
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/specs/petstore.yaml',
                'ProjectB/ProjectB_0/specs'
            );

            // Verify info messages
            expect(messageHelper.showInfo).toHaveBeenCalledWith(
                'API added: ProjectB/api-assets/payment-api.yml'
            );
            expect(messageHelper.showInfo).toHaveBeenCalledWith(
                'Spec added: ProjectB/ProjectB_0/specs/petstore.yaml'
            );
        });

        it('should show error when source project is not found', () => {
            const cachedAssetNoSource: AssetCacheModel = {
                kind: 'api',
                ref: 'dev:PaymentAPI:1.0',
                isNewlyAdded: false,
                sourceProject: undefined,
            };

            bundleApiDependency(
                mockApiAsset,
                mockDirent,
                cachedAssetNoSource,
                '/root',
                'ProjectB',
                mockZip
            );

            expect(messageHelper.showError).toHaveBeenCalledWith(
                'Source project not found for API dependency dev:PaymentAPI:1.0'
            );
            expect(mockZip.addLocalFile).not.toHaveBeenCalled();
        });

        it('should bundle API even when spec is missing', () => {
            existsSyncSpy.mockReturnValue(false);

            bundleApiDependency(
                mockApiAsset,
                mockDirent,
                mockCachedAsset,
                '/root',
                'ProjectB',
                mockZip
            );

            // Verify API was added
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/api-assets/payment-api.yml',
                'ProjectB/ProjectB_0/api-assets'
            );

            // Verify warning for missing spec
            expect(messageHelper.showWarning).toHaveBeenCalledWith(
                'API spec not found: /root/ProjectB/specs/petstore.yaml for API dev:PaymentAPI:1.0'
            );

            // Spec should not be added
            expect(mockZip.addLocalFile).toHaveBeenCalledTimes(1);
        });

        it('should handle API with complex nested structure', () => {
            const nestedDirent = {
                name: 'api.yml',
                parentPath: '/root/ProjectB/apis/v1/payment',
                path: '/root/ProjectB/apis/v1/payment',
                isDirectory: () => false,
                isFile: () => true,
                isBlockDevice: () => false,
                isCharacterDevice: () => false,
                isFIFO: () => false,
                isSocket: () => false,
                isSymbolicLink: () => false,
                [Symbol.toStringTag]: 'Dirent',
            } as unknown as fs.Dirent;

            const nestedApiAsset = {
                ...mockApiAsset,
                spec: {
                    'api-spec': {
                        $path: '../../specs/payment.yaml',
                    },
                },
            } as unknown as BaseAsset;

            bundleApiDependency(
                nestedApiAsset,
                nestedDirent,
                mockCachedAsset,
                '/root',
                'ProjectB',
                mockZip
            );

            // Verify API was added with correct path
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/apis/v1/payment/api.yml',
                'ProjectB/ProjectB_0/apis/v1/payment'
            );

            // Verify spec was resolved correctly
            // From apis/v1/payment, going up ../../ lands in apis/, then specs/payment.yaml
            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/apis/specs/payment.yaml',
                'ProjectB/ProjectB_0/apis/specs'
            );
        });

        it('should use unique timestamp for each bundle', () => {
            const timestamps = [1, 2];
            let callCount = 0;
            jest.spyOn(Date, 'now').mockImplementation(() => timestamps[callCount++]);

            // First call
            bundleApiDependency(
                mockApiAsset,
                mockDirent,
                mockCachedAsset,
                '/root',
                'ProjectB',
                mockZip
            );

            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/api-assets/payment-api.yml',
                'ProjectB/ProjectB_1/api-assets'
            );

            jest.clearAllMocks();

            // Second call
            bundleApiDependency(
                mockApiAsset,
                mockDirent,
                mockCachedAsset,
                '/root',
                'ProjectB',
                mockZip
            );

            expect(mockZip.addLocalFile).toHaveBeenCalledWith(
                '/root/ProjectB/api-assets/payment-api.yml',
                'ProjectB/ProjectB_2/api-assets'
            );
        });
    });
});