/*
 * Runtime Web – Unified Serverless Framework for Full-Stack TypeScript Applications
 * Documentation: https://runtimeweb.com/docs/getting-started
 *
 * @template: ai
 */

/**
 * Property-Based Tests for AI Stack CDK Stage Configuration
 *
 * Property 8: CDK Stage-Based Configuration
 *
 * These tests verify that the AI stack template correctly configures
 * resources based on deployment stage (dev, staging, prod).
 *
 * Architecture Note:
 * - DynamoDB session table is in infra-stack.ts (base infrastructure)
 * - OpenSearch and Bedrock permissions are in ai-stack.ts (AI-specific)
 * This separation prevents cyclic dependencies between stacks.
 */

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

/**
 * Stage type matching the CDK stack configuration
 */
type Stage = 'dev' | 'staging' | 'prod';

/**
 * Expected removal policy based on stage
 */
type RemovalPolicy = 'DESTROY' | 'RETAIN';

/**
 * Expected instance type based on stage
 */
interface StageInstanceConfig {
    dataNodeInstanceType: string;
    dataNodes: number;
    multiAzWithStandbyEnabled: boolean;
    ebsVolumeSize: number;
}

/**
 * Parses the AI stack template and extracts stage-based configuration patterns
 */
function parseAiStackTemplate(): string {
    const templatePath = path.join(
        __dirname,
        '..',
        'lib',
        'ai-stack.ts.template',
    );
    return fs.readFileSync(templatePath, 'utf-8');
}

/**
 * Parses the Infra stack template for DynamoDB configuration
 */
function parseInfraStackTemplate(): string {
    const templatePath = path.join(
        __dirname,
        '..',
        'lib',
        'infra-stack.ts.template',
    );
    return fs.readFileSync(templatePath, 'utf-8');
}

/**
 * Extracts removal policy logic from template content
 * Returns the expected removal policy for a given stage
 */
function getExpectedRemovalPolicy(stage: Stage): RemovalPolicy {
    // Based on the template logic:
    // removalPolicy: props.stage === 'prod' ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY
    return stage === 'prod' ? 'RETAIN' : 'DESTROY';
}

/**
 * Extracts expected instance configuration for a given stage
 */
function getExpectedInstanceConfig(stage: Stage): StageInstanceConfig {
    // Based on the template logic:
    // dataNodeInstanceType: config.stage === 'prod' ? 'r6g.large.search' : 't3.small.search'
    // dataNodes: config.stage === 'prod' ? 2 : 1
    // multiAzWithStandbyEnabled: config.stage === 'prod'
    // volumeSize: config.stage === 'prod' ? 100 : 10
    return {
        dataNodeInstanceType: stage === 'prod' ? 'r6g.large.search' : 't3.small.search',
        dataNodes: stage === 'prod' ? 2 : 1,
        multiAzWithStandbyEnabled: stage === 'prod',
        ebsVolumeSize: stage === 'prod' ? 100 : 10,
    };
}

describe('AI Stack - Property-Based Tests', () => {
    /**
     * Property 8: CDK Stage-Based Configuration
     *
     * For any stage configuration:
     * 1. Dev stage should use DESTROY removal policy for OpenSearch
     * 2. Staging stage should use DESTROY removal policy
     * 3. Prod stage should use RETAIN removal policy for OpenSearch
     * 4. Stage-specific instance types should be applied (smaller for dev, larger for prod)
     *
     * Note: DynamoDB removal policy is tested in infra-stack tests
     */
    describe('Property 8: CDK Stage-Based Configuration', () => {
        const templateContent = parseAiStackTemplate();
        const infraTemplateContent = parseInfraStackTemplate();

        it('should contain stage-based removal policy logic for DynamoDB in infra-stack', () => {
            // DynamoDB is in infra-stack, not ai-stack
            // Verify the infra template contains the correct conditional logic for DynamoDB removal policy
            const dynamoDbRemovalPolicyPattern =
                /removalPolicy:\s*props\.stage\s*===\s*['"]prod['"]\s*\?\s*cdk\.RemovalPolicy\.RETAIN\s*:\s*cdk\.RemovalPolicy\.DESTROY/;

            expect(infraTemplateContent).toMatch(dynamoDbRemovalPolicyPattern);
        });

        it('should contain stage-based removal policy logic for OpenSearch', () => {
            // Verify the template contains the correct conditional logic for OpenSearch removal policy
            const removalPolicyPattern =
                /removalPolicy:\s*config\.stage\s*===\s*['"]prod['"]\s*\?\s*cdk\.RemovalPolicy\.RETAIN\s*:\s*cdk\.RemovalPolicy\.DESTROY/;

            expect(templateContent).toMatch(removalPolicyPattern);
        });

        it('should contain stage-based instance type configuration for OpenSearch', () => {
            // Verify the template contains conditional logic for instance types
            const instanceTypePattern =
                /dataNodeInstanceType:\s*config\.stage\s*===\s*['"]prod['"]\s*\?\s*['"]r6g\.large\.search['"]\s*:\s*['"]t3\.small\.search['"]/;

            expect(templateContent).toMatch(instanceTypePattern);
        });

        it('should contain stage-based data node count configuration', () => {
            // Verify the template contains conditional logic for data node count
            const dataNodesPattern =
                /dataNodes:\s*config\.stage\s*===\s*['"]prod['"]\s*\?\s*2\s*:\s*1/;

            expect(templateContent).toMatch(dataNodesPattern);
        });

        it('should contain stage-based multi-AZ configuration', () => {
            // Verify the template contains conditional logic for multi-AZ
            const multiAzPattern =
                /multiAzWithStandbyEnabled:\s*config\.stage\s*===\s*['"]prod['"]/;

            expect(templateContent).toMatch(multiAzPattern);
        });

        it('should contain stage-based EBS volume size configuration', () => {
            // Verify the template contains conditional logic for EBS volume size
            const volumeSizePattern =
                /volumeSize:\s*config\.stage\s*===\s*['"]prod['"]\s*\?\s*100\s*:\s*10/;

            expect(templateContent).toMatch(volumeSizePattern);
        });

        /**
         * Property test: For any valid stage, the expected removal policy should be correct
         */
        it('property: removal policy is DESTROY for non-prod stages, RETAIN for prod', () => {
            fc.assert(
                fc.property(
                    fc.constantFrom<Stage>('dev', 'staging', 'prod'),
                    (stage) => {
                        const expectedPolicy = getExpectedRemovalPolicy(stage);

                        // Property: prod stage should use RETAIN
                        if (stage === 'prod') {
                            expect(expectedPolicy).toBe('RETAIN');
                        } else {
                            // Property: non-prod stages (dev, staging) should use DESTROY
                            expect(expectedPolicy).toBe('DESTROY');
                        }

                        return true;
                    },
                ),
                { numRuns: 100 },
            );
        });

        /**
         * Property test: For any valid stage, instance configuration should be appropriate
         */
        it('property: instance configuration scales appropriately with stage', () => {
            fc.assert(
                fc.property(
                    fc.constantFrom<Stage>('dev', 'staging', 'prod'),
                    (stage) => {
                        const config = getExpectedInstanceConfig(stage);

                        if (stage === 'prod') {
                            // Property: prod should have larger instances
                            expect(config.dataNodeInstanceType).toBe('r6g.large.search');
                            expect(config.dataNodes).toBe(2);
                            expect(config.multiAzWithStandbyEnabled).toBe(true);
                            expect(config.ebsVolumeSize).toBe(100);
                        } else {
                            // Property: non-prod should have smaller instances
                            expect(config.dataNodeInstanceType).toBe('t3.small.search');
                            expect(config.dataNodes).toBe(1);
                            expect(config.multiAzWithStandbyEnabled).toBe(false);
                            expect(config.ebsVolumeSize).toBe(10);
                        }

                        return true;
                    },
                ),
                { numRuns: 100 },
            );
        });

        /**
         * Property test: Stage configuration is consistent across all resources
         */
        it('property: stage configuration is consistent across DynamoDB and OpenSearch', () => {
            fc.assert(
                fc.property(
                    fc.constantFrom<Stage>('dev', 'staging', 'prod'),
                    (stage) => {
                        const removalPolicy = getExpectedRemovalPolicy(stage);
                        const instanceConfig = getExpectedInstanceConfig(stage);

                        // Property: If removal policy is RETAIN, we should be in prod
                        if (removalPolicy === 'RETAIN') {
                            expect(stage).toBe('prod');
                            expect(instanceConfig.multiAzWithStandbyEnabled).toBe(true);
                        }

                        // Property: If removal policy is DESTROY, we should NOT be in prod
                        if (removalPolicy === 'DESTROY') {
                            expect(stage).not.toBe('prod');
                            expect(instanceConfig.multiAzWithStandbyEnabled).toBe(false);
                        }

                        return true;
                    },
                ),
                { numRuns: 100 },
            );
        });

        /**
         * Property test: Service name and stage are used in resource naming
         */
        it('property: resource names include service name and stage', () => {
            fc.assert(
                fc.property(
                    fc.record({
                        serviceName: fc.stringMatching(/^[a-z][a-z0-9-]{2,20}$/),
                        stage: fc.constantFrom<Stage>('dev', 'staging', 'prod'),
                    }),
                    ({ serviceName, stage }) => {
                        // Verify infra template contains patterns for DynamoDB naming
                        // tableName: `${props.serviceName}-sessions-${props.stage}`
                        const tableNamePattern = /tableName:\s*`\$\{props\.serviceName\}-sessions-\$\{props\.stage\}`/;
                        expect(infraTemplateContent).toMatch(tableNamePattern);

                        // Verify ai-stack template contains patterns for OpenSearch naming
                        // domainName: `${config.serviceName}-search-${config.stage}`.toLowerCase()
                        const domainNamePattern = /domainName:\s*`\$\{config\.serviceName\}-search-\$\{config\.stage\}`\.toLowerCase\(\)/;
                        expect(templateContent).toMatch(domainNamePattern);

                        // Verify the naming convention would produce valid names
                        const expectedTableName = `${serviceName}-sessions-${stage}`;
                        const expectedDomainName = `${serviceName}-search-${stage}`.toLowerCase();

                        // Property: Table name should be valid (non-empty, contains stage)
                        expect(expectedTableName.length).toBeGreaterThan(0);
                        expect(expectedTableName).toContain(stage);

                        // Property: Domain name should be valid (lowercase, contains stage)
                        expect(expectedDomainName).toBe(expectedDomainName.toLowerCase());
                        expect(expectedDomainName).toContain(stage);

                        return true;
                    },
                ),
                { numRuns: 50 },
            );
        });

        /**
         * Property test: Point-in-time recovery is enabled only for prod (in infra-stack)
         */
        it('property: point-in-time recovery is enabled only for prod', () => {
            // Verify the infra template contains conditional logic for PITR
            const pitrPattern = /pointInTimeRecovery:\s*props\.stage\s*===\s*['"]prod['"]/;
            expect(infraTemplateContent).toMatch(pitrPattern);

            fc.assert(
                fc.property(
                    fc.constantFrom<Stage>('dev', 'staging', 'prod'),
                    (stage) => {
                        // Property: PITR should be enabled only for prod
                        const pitrEnabled = stage === 'prod';

                        if (stage === 'prod') {
                            expect(pitrEnabled).toBe(true);
                        } else {
                            expect(pitrEnabled).toBe(false);
                        }

                        return true;
                    },
                ),
                { numRuns: 100 },
            );
        });

        /**
         * Property test: Logging configuration varies by stage
         */
        it('property: slow logs are disabled for dev stage', () => {
            // Verify the template contains conditional logic for logging
            const slowSearchLogPattern = /slowSearchLogEnabled:\s*config\.stage\s*!==\s*['"]dev['"]/;
            const slowIndexLogPattern = /slowIndexLogEnabled:\s*config\.stage\s*!==\s*['"]dev['"]/;

            expect(templateContent).toMatch(slowSearchLogPattern);
            expect(templateContent).toMatch(slowIndexLogPattern);

            fc.assert(
                fc.property(
                    fc.constantFrom<Stage>('dev', 'staging', 'prod'),
                    (stage) => {
                        // Property: Slow logs should be disabled for dev, enabled for staging/prod
                        const slowLogsEnabled = stage !== 'dev';

                        if (stage === 'dev') {
                            expect(slowLogsEnabled).toBe(false);
                        } else {
                            expect(slowLogsEnabled).toBe(true);
                        }

                        return true;
                    },
                ),
                { numRuns: 100 },
            );
        });
    });

    /**
     * Additional property tests for CloudFormation outputs
     */
    describe('CloudFormation Outputs', () => {
        const templateContent = parseAiStackTemplate();
        const infraTemplateContent = parseInfraStackTemplate();

        it('should export session table name and ARN in infra-stack', () => {
            // DynamoDB outputs are in infra-stack
            expect(infraTemplateContent).toMatch(/CfnOutput\(this,\s*['"]SessionTableName['"]/);
            expect(infraTemplateContent).toMatch(/CfnOutput\(this,\s*['"]SessionTableArn['"]/);
        });

        it('should export search domain endpoint and ARN in ai-stack', () => {
            expect(templateContent).toMatch(/CfnOutput\(this,\s*['"]SearchDomainEndpoint['"]/);
            expect(templateContent).toMatch(/CfnOutput\(this,\s*['"]SearchDomainArn['"]/);
        });

        it('property: export names include service name and stage', () => {
            fc.assert(
                fc.property(
                    fc.record({
                        serviceName: fc.stringMatching(/^[a-z][a-z0-9-]{2,20}$/),
                        stage: fc.constantFrom<Stage>('dev', 'staging', 'prod'),
                    }),
                    ({ serviceName, stage }) => {
                        // Verify ai-stack template contains patterns for dynamic export names
                        const exportNamePattern = /exportName:\s*`\$\{config\.serviceName\}-\w+-\$\{config\.stage\}`/;
                        expect(templateContent).toMatch(exportNamePattern);

                        // Property: Export names should be unique per service/stage combination
                        const expectedExportPrefix = `${serviceName}-`;
                        const expectedExportSuffix = `-${stage}`;

                        expect(expectedExportPrefix.length).toBeGreaterThan(1);
                        expect(expectedExportSuffix.length).toBeGreaterThan(1);

                        return true;
                    },
                ),
                { numRuns: 50 },
            );
        });
    });

    /**
     * IAM Permissions property tests
     */
    describe('IAM Permissions', () => {
        const templateContent = parseAiStackTemplate();
        const infraTemplateContent = parseInfraStackTemplate();

        it('should grant DynamoDB read/write permissions to Lambda role in infra-stack', () => {
            // DynamoDB permissions are granted in infra-stack
            expect(infraTemplateContent).toMatch(/sessionTable\.grantReadWriteData\(this\.lambdaRole\)/);
        });

        it('should grant OpenSearch read/write permissions to Lambda role in ai-stack', () => {
            expect(templateContent).toMatch(/searchDomain\.grantReadWrite\(lambdaRole\)/);
        });

        it('should grant Bedrock invoke permissions to Lambda role in ai-stack', () => {
            expect(templateContent).toMatch(/bedrock:InvokeModel/);
            expect(templateContent).toMatch(/bedrock:InvokeModelWithResponseStream/);
        });

        it('property: Bedrock permissions use regional ARN pattern', () => {
            // Verify the template uses proper regional ARN pattern for Bedrock
            const bedrockArnPattern = /arn:aws:bedrock:\$\{this\.region\}::inference-profile\/\*/;
            expect(templateContent).toMatch(bedrockArnPattern);
        });
    });
});
