/*
 * Elastic License 2.0
 * Copyright (C) 2025–present Raman Marozau, Work Target Insight Function. All rights reserved.
 * Contact: raman@worktif.com
 *
 * This file is part of the Licensed Work: worktif.runtime.cdk <worktif_runtime_cdk>.
 * Use of this software is governed by the Elastic License 2.0; see the LICENSE file
 * or https://www.elastic.co/licensing/elastic-license for details.
 *
 * SPDX-License-Identifier: Elastic-2.0
 */

/**
 * Property-based tests for AI Template Scaffolding
 *
 * **Property 1: Template Scaffolding Produces Valid Directory Structure**
 * **Validates: Requirements 1.4, 1.5**
 *
 * This test verifies that the AI template scaffolding produces a valid directory
 * structure that matches the expected template layout and contains all required files.
 */

import { describe, expect, it, beforeAll } from '@jest/globals';
import * as fc from 'fast-check';
import * as fs from 'fs';
import * as path from 'path';

/**
 * Template types available in the codebase.
 */
type TemplateType = 'basic' | 'advanced' | 'ai';

/**
 * Represents a file or directory in the template structure.
 */
interface TemplateEntry {
    name: string;
    type: 'file' | 'directory';
    path: string;
}

/**
 * Required structure for the AI template application directory.
 */
const REQUIRED_APPLICATION_STRUCTURE: TemplateEntry[] = [
    { name: 'package.json', type: 'file', path: '' },
    { name: 'README.md', type: 'file', path: '' },
    { name: 'runtime.config.ts', type: 'file', path: '' },
    { name: 'tsconfig.json', type: 'file', path: '' },
    { name: '.env.example', type: 'file', path: '' },
    { name: 'gitignore.template', type: 'file', path: '' },
    { name: 'src', type: 'directory', path: '' },
    { name: 'public', type: 'directory', path: '' },
    { name: 'handlers', type: 'directory', path: 'src' },
    { name: 'services', type: 'directory', path: 'src' },
    { name: 'ties', type: 'directory', path: 'src' },
    { name: 'types', type: 'directory', path: 'src' },
    { name: 'chat.handlers.ts', type: 'file', path: 'src/handlers' },
    { name: 'bedrock.service.ts', type: 'file', path: 'src/services' },
    { name: 'memory.service.ts', type: 'file', path: 'src/services' },
    { name: 'search.service.ts', type: 'file', path: 'src/services' },
    { name: 'ai.ties.ts', type: 'file', path: 'src/ties' },
    { name: 'index.ts', type: 'file', path: 'src/ties' },
    { name: 'chat.types.ts', type: 'file', path: 'src/types' },
    { name: 'App.tsx', type: 'file', path: 'src' },
    { name: 'index.tsx', type: 'file', path: 'src' },
    { name: 'routes.tsx', type: 'file', path: 'src' },
    { name: 'index.html', type: 'file', path: 'public' },
];

/**
 * Required structure for the AI template CDK directory.
 */
const REQUIRED_CDK_STRUCTURE: TemplateEntry[] = [
    { name: 'package.json.template', type: 'file', path: '' },
    { name: 'README.md.template', type: 'file', path: '' },
    { name: 'tsconfig.json.template', type: 'file', path: '' },
    { name: 'cdk.json.template', type: 'file', path: '' },
    { name: '.gitignore.template', type: 'file', path: '' },
    { name: 'bin', type: 'directory', path: '' },
    { name: 'lib', type: 'directory', path: '' },
    { name: 'app.ts.template', type: 'file', path: 'bin' },
    { name: 'ai-stack.ts.template', type: 'file', path: 'lib' },
    { name: 'config.ts.template', type: 'file', path: 'lib' },
    { name: 'infra-stack.ts.template', type: 'file', path: 'lib' },
    { name: 'web-stack.ts.template', type: 'file', path: 'lib' },
    { name: 'runtime-stack.ts.template', type: 'file', path: 'lib' },
    { name: 'awake-stack.ts.template', type: 'file', path: 'lib' },
];

/**
 * Gets the path to a template directory.
 *
 * @param templateType - The template type
 * @returns The absolute path to the template directory
 */
function getTemplatePath(templateType: TemplateType): string {
    return path.join(__dirname, '..', templateType);
}

/**
 * Gets the application directory path for a template.
 *
 * @param templateType - The template type
 * @returns The absolute path to the application directory
 */
function getApplicationPath(templateType: TemplateType): string {
    return path.join(getTemplatePath(templateType), 'application');
}

/**
 * Gets the CDK directory path for a template.
 *
 * @param templateType - The template type
 * @returns The absolute path to the CDK directory
 */
function getCdkPath(templateType: TemplateType): string {
    return path.join(getTemplatePath(templateType), 'cdk');
}

/**
 * Checks if a template entry exists at the expected location.
 *
 * @param basePath - The base path to check from
 * @param entry - The template entry to verify
 * @returns True if the entry exists and matches the expected type
 */
function entryExists(basePath: string, entry: TemplateEntry): boolean {
    const fullPath = entry.path
        ? path.join(basePath, entry.path, entry.name)
        : path.join(basePath, entry.name);

    if (!fs.existsSync(fullPath)) {
        return false;
    }

    const stat = fs.statSync(fullPath);
    return entry.type === 'directory' ? stat.isDirectory() : stat.isFile();
}

/**
 * Gets all template types that have the AI template structure.
 *
 * @returns Array of template types with AI template
 */
function getAiTemplateTypes(): TemplateType[] {
    const aiPath = getTemplatePath('ai');
    return fs.existsSync(aiPath) ? ['ai'] : [];
}

/**
 * Reads a file content from the template.
 *
 * @param templateType - The template type
 * @param relativePath - The relative path within the template
 * @returns The file content or empty string if not found
 */
function readTemplateFile(templateType: TemplateType, relativePath: string): string {
    const fullPath = path.join(getTemplatePath(templateType), relativePath);
    return fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf-8') : '';
}

describe('AI Template Scaffolding Property Tests', () => {
    describe('Property 1: Template Scaffolding Produces Valid Directory Structure', () => {
        let availableAiTemplates: TemplateType[];

        beforeAll(() => {
            availableAiTemplates = getAiTemplateTypes();
        });

        /**
         * **Property 1: Template Scaffolding Produces Valid Directory Structure**
         * **Validates: Requirements 1.4, 1.5**
         *
         * For the AI template, the scaffolded structure SHALL contain all required
         * application files and directories as specified in the design document.
         */
        it('should contain all required application structure entries', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            fc.assert(
                fc.property(
                    fc.constantFrom(...REQUIRED_APPLICATION_STRUCTURE),
                    (entry: TemplateEntry) => {
                        const applicationPath = getApplicationPath('ai');
                        const exists = entryExists(applicationPath, entry);

                        expect(exists).toBe(true);

                        return true;
                    }
                ),
                { numRuns: REQUIRED_APPLICATION_STRUCTURE.length }
            );
        });

        /**
         * **Validates: Requirements 1.4, 1.5**
         *
         * For the AI template, the scaffolded structure SHALL contain all required
         * CDK files and directories as specified in the design document.
         */
        it('should contain all required CDK structure entries', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            fc.assert(
                fc.property(
                    fc.constantFrom(...REQUIRED_CDK_STRUCTURE),
                    (entry: TemplateEntry) => {
                        const cdkPath = getCdkPath('ai');
                        const exists = entryExists(cdkPath, entry);

                        expect(exists).toBe(true);

                        return true;
                    }
                ),
                { numRuns: REQUIRED_CDK_STRUCTURE.length }
            );
        });

        /**
         * **Validates: Requirement 1.5**
         *
         * The AI template application directory SHALL follow the same structure
         * as the basic template (src/, public/, package.json, tsconfig.json, runtime.config.ts).
         */
        it('should follow the same top-level structure as basic template', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const basicAppPath = getApplicationPath('basic');
            const aiAppPath = getApplicationPath('ai');

            // Skip if basic template doesn't exist
            if (!fs.existsSync(basicAppPath)) {
                console.warn('Basic template not found, skipping comparison');
                return;
            }

            const basicTopLevel = fs.readdirSync(basicAppPath, { withFileTypes: true })
                .filter(d => d.isDirectory())
                .map(d => d.name)
                .sort();

            const aiTopLevel = fs.readdirSync(aiAppPath, { withFileTypes: true })
                .filter(d => d.isDirectory())
                .map(d => d.name)
                .sort();

            // AI template should have at least the same directories as basic
            fc.assert(
                fc.property(
                    fc.constantFrom(...basicTopLevel),
                    (dirName: string) => {
                        expect(aiTopLevel).toContain(dirName);
                        return true;
                    }
                ),
                { numRuns: basicTopLevel.length }
            );
        });
    });

    describe('Property 2: Template Files Contain Valid Content', () => {
        let availableAiTemplates: TemplateType[];

        beforeAll(() => {
            availableAiTemplates = getAiTemplateTypes();
        });

        /**
         * **Validates: Requirements 8.1, 8.2, 8.3, 8.4**
         *
         * The package.json SHALL contain all required AI-specific dependencies.
         */
        it('should have package.json with required AI dependencies', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const requiredDeps = [
                '@aws-sdk/client-bedrock-runtime',
                '@aws-sdk/client-dynamodb',
                '@aws-sdk/lib-dynamodb',
                '@opensearch-project/opensearch',
                '@aws-sdk/credential-provider-node',
            ];

            const packageContent = readTemplateFile('ai', 'application/package.json');
            const packageJson = JSON.parse(packageContent);
            const deps = packageJson.dependencies || {};

            fc.assert(
                fc.property(
                    fc.constantFrom(...requiredDeps),
                    (depName: string) => {
                        expect(deps).toHaveProperty(depName);
                        return true;
                    }
                ),
                { numRuns: requiredDeps.length }
            );
        });

        /**
         * **Validates: Requirement 8.6**
         *
         * The package.json SHALL use template variables for project name substitution.
         */
        it('should have package.json with template variables', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const packageContent = readTemplateFile('ai', 'application/package.json');

            expect(packageContent).toContain('{{projectName}}');
        });

        /**
         * **Validates: Requirements 9.1, 9.2, 9.3, 9.4, 9.6**
         *
         * The .env.example SHALL contain all required environment variables.
         */
        it('should have .env.example with required environment variables', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const requiredEnvVars = [
                'BEDROCK_MODEL_ID',
                'MEMORY_TABLE_NAME',
                'OPENSEARCH_ENDPOINT',
                'AWS_REGION',
            ];

            const envContent = readTemplateFile('ai', 'application/.env.example');

            fc.assert(
                fc.property(
                    fc.constantFrom(...requiredEnvVars),
                    (envVar: string) => {
                        expect(envContent).toContain(envVar);
                        return true;
                    }
                ),
                { numRuns: requiredEnvVars.length }
            );
        });

        /**
         * **Validates: Requirements 3.2**
         *
         * The BedrockService SHALL use AWS SDK v3 modular imports.
         */
        it('should have BedrockService with AWS SDK v3 imports', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const bedrockContent = readTemplateFile('ai', 'application/src/services/bedrock.service.ts');

            // Verify AWS SDK v3 modular import
            expect(bedrockContent).toMatch(
                /import\s*\{[^}]*BedrockRuntimeClient[^}]*\}\s*from\s*['"]@aws-sdk\/client-bedrock-runtime['"]/s
            );

            // Verify no AWS SDK v2 import
            expect(bedrockContent).not.toContain("from 'aws-sdk'");
            expect(bedrockContent).not.toContain('from "aws-sdk"');
        });

        /**
         * **Validates: Requirements 4.2**
         *
         * The MemoryService SHALL use AWS SDK v3 modular imports.
         */
        it('should have MemoryService with AWS SDK v3 imports', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const memoryContent = readTemplateFile('ai', 'application/src/services/memory.service.ts');

            // Verify AWS SDK v3 modular imports
            expect(memoryContent).toMatch(
                /import\s*\{[^}]*DynamoDBClient[^}]*\}\s*from\s*['"]@aws-sdk\/client-dynamodb['"]/s
            );
            expect(memoryContent).toMatch(
                /import\s*\{[^}]*DynamoDBDocumentClient[^}]*\}\s*from\s*['"]@aws-sdk\/lib-dynamodb['"]/s
            );

            // Verify no AWS SDK v2 import
            expect(memoryContent).not.toContain("from 'aws-sdk'");
            expect(memoryContent).not.toContain('from "aws-sdk"');
        });

        /**
         * **Validates: Requirements 5.2**
         *
         * The SearchService SHALL use the OpenSearch client for AWS.
         */
        it('should have SearchService with OpenSearch client', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const searchContent = readTemplateFile('ai', 'application/src/services/search.service.ts');

            // Verify OpenSearch client import
            expect(searchContent).toMatch(
                /import\s*\{[^}]*Client[^}]*\}\s*from\s*['"]@opensearch-project\/opensearch['"]/s
            );
        });
    });

    describe('Property 3: CDK Stack Configuration', () => {
        let availableAiTemplates: TemplateType[];

        beforeAll(() => {
            availableAiTemplates = getAiTemplateTypes();
        });

        /**
         * **Validates: Requirements 6.2, 6.3**
         *
         * The AI CDK stack SHALL create OpenSearch domain.
         * DynamoDB table is created in infra-stack to prevent cyclic dependencies.
         */
        it('should have AI stack with OpenSearch resources and infra-stack with DynamoDB', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const aiStackContent = readTemplateFile('ai', 'cdk/lib/ai-stack.ts.template');
            const infraStackContent = readTemplateFile('ai', 'cdk/lib/infra-stack.ts.template');

            // Verify DynamoDB table creation is in infra-stack
            expect(infraStackContent).toContain('dynamodb.Table');
            expect(infraStackContent).toContain('sessionId');

            // Verify OpenSearch domain creation is in ai-stack
            expect(aiStackContent).toContain('opensearch.Domain');
        });

        /**
         * **Validates: Requirements 6.4**
         *
         * The AI CDK stack SHALL configure IAM permissions for Bedrock.
         */
        it('should have AI stack with Bedrock IAM permissions', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const aiStackContent = readTemplateFile('ai', 'cdk/lib/ai-stack.ts.template');

            // Verify Bedrock permissions
            expect(aiStackContent).toContain('bedrock:InvokeModel');
        });

        /**
         * **Validates: Requirements 6.6, 6.8**
         *
         * The AI CDK stack SHALL support stage-based configuration with appropriate
         * removal policies (DESTROY for dev, RETAIN for prod).
         */
        it('should have AI stack with stage-based removal policies', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const aiStackContent = readTemplateFile('ai', 'cdk/lib/ai-stack.ts.template');

            // Verify stage-based removal policy logic
            expect(aiStackContent).toContain('RemovalPolicy.DESTROY');
            expect(aiStackContent).toContain('RemovalPolicy.RETAIN');
            expect(aiStackContent).toMatch(/stage\s*===\s*['"]prod['"]/);
        });

        /**
         * **Validates: Requirements 6.7**
         *
         * The AI CDK stack SHALL export resource ARNs and endpoints as CloudFormation outputs.
         * Note: DynamoDB outputs (SessionTableName) are in infra-stack.ts, not ai-stack.ts
         * This separation prevents cyclic dependencies between stacks.
         */
        it('should have AI stack with CloudFormation outputs', () => {
            if (availableAiTemplates.length === 0) {
                console.warn('AI template not found, skipping test');
                return;
            }

            const aiStackContent = readTemplateFile('ai', 'cdk/lib/ai-stack.ts.template');
            const infraStackContent = readTemplateFile('ai', 'cdk/lib/infra-stack.ts.template');

            // Verify CloudFormation outputs in ai-stack (OpenSearch only)
            expect(aiStackContent).toContain('CfnOutput');
            expect(aiStackContent).toContain('SearchDomainEndpoint');
            expect(aiStackContent).toContain('SearchDomainArn');

            // Verify DynamoDB outputs are in infra-stack
            expect(infraStackContent).toContain('CfnOutput');
            expect(infraStackContent).toContain('SessionTableName');
            expect(infraStackContent).toContain('SessionTableArn');
        });
    });
});
