// Load complete nerdamer with all modules
import nerdamerRuntime from '../all';
import tsMorph from 'ts-morph';

// Type alias for dynamic property access in reflection tests

type Indexable = Record<string, any>;
const nerdamer = nerdamerRuntime as typeof nerdamerRuntime & Indexable;

describe('Nerdamer API Surface Reflection Test', () => {
    let tsApiExports: string[];
    let tsExportedDeclarations: ReadonlyMap<string, tsMorph.ExportedDeclarations[]>;
    let project: tsMorph.Project;
    let sourceFile: tsMorph.SourceFile;
    let nerdamerNamespace: tsMorph.ModuleDeclaration;
    const jsApiExports = Object.keys(nerdamerRuntime).sort();

    beforeAll(() => {
        project = new tsMorph.Project({
            tsConfigFilePath: 'tsconfig.json',
        });

        sourceFile = project.getSourceFileOrThrow('index.d.ts');
        nerdamerNamespace = sourceFile.getModuleOrThrow('nerdamer');

        // Get all exported declarations for detailed analysis
        tsExportedDeclarations = nerdamerNamespace.getExportedDeclarations();

        // Get all exported functions, variables, etc. from the namespace
        tsApiExports = Array.from(tsExportedDeclarations.keys())
            .filter(
                (name: string) =>
                    // Filter out internal types and interfaces that aren't expected on the runtime object
                    // NerdamerSet is an exception - it's a renamed Set to avoid conflicts with native Set
                    (!name.startsWith('Nerdamer') || name === 'NerdamerSet') &&
                    !name.endsWith('Constructor') &&
                    !name.includes('Interface') &&
                    name !== 'NerdamerCore' &&
                    // Filter out type-only exports (type aliases) that have no runtime representation
                    ![
                        'OutputType',
                        'ParseOption',
                        'ExpressionParam',
                        'ExpandOptions',
                        'ArithmeticOperand',
                        'LaTeXToken',
                        'FilteredLaTeXToken',
                        'SortFn',
                        'SolveResult',
                    ].includes(name)
            )
            .sort();
    });

    it('should have a type definition for every runtime property', () => {
        const missingInTs = jsApiExports.filter(p => !tsApiExports.includes(p));

        if (missingInTs.length > 0) {
            console.log('Missing in TypeScript definitions:', missingInTs);
        }

        expect(missingInTs).toEqual([]);
    });

    it('should have a runtime implementation for every type definition', () => {
        const missingInJs = tsApiExports.filter(p => !jsApiExports.includes(p));

        if (missingInJs.length > 0) {
            console.log('Missing in JavaScript runtime:', missingInJs);
        }

        expect(missingInJs).toEqual([]);
    });

    it('should have correct function vs property distinction', () => {
        const typeErrors: string[] = [];
        const importAliasInfo: string[] = [];

        for (const exportName of tsApiExports) {
            const runtimeValue = nerdamer[exportName];
            const tsDeclarations = tsExportedDeclarations.get(exportName) || [];

            if (tsDeclarations.length === 0) {
                continue;
            }

            const tsDeclaration = tsDeclarations[0]!;
            const isRuntimeFunction = typeof runtimeValue === 'function';

            // Check if TS declaration indicates it's a function
            let isTsFunction = false;

            // Use type guard for declarations that have getKind
            const declarationWithKind = tsDeclaration as { getKind?: () => number; getKindName?: () => string };
            if (declarationWithKind.getKind) {
                const kind = declarationWithKind.getKind();
                const kindName = declarationWithKind.getKindName?.() || 'Unknown';

                // Handle direct function declarations
                if (kind === tsMorph.SyntaxKind.FunctionDeclaration || kind === tsMorph.SyntaxKind.MethodSignature) {
                    isTsFunction = true;
                }

                // Handle function type property signatures
                else if (kind === tsMorph.SyntaxKind.PropertySignature) {
                    // Cast to PropertySignature to access getTypeNode (must go through unknown first)
                    const propSig = tsDeclaration as unknown as tsMorph.PropertySignature;
                    if (propSig.getTypeNode?.()?.getKind() === tsMorph.SyntaxKind.FunctionType) {
                        isTsFunction = true;
                    }
                }

                // Handle export import assignments (like: export import cos = nerdamerPrime.cos)
                // These are the most common pattern in this codebase
                else if (
                    kind === tsMorph.SyntaxKind.ImportEqualsDeclaration ||
                    kind === tsMorph.SyntaxKind.ImportClause ||
                    kind === tsMorph.SyntaxKind.ImportDeclaration ||
                    kind === tsMorph.SyntaxKind.ExportDeclaration
                ) {
                    // For import/export declarations, we need to check what they're importing
                    // In this case, we assume they're importing functions from nerdamerPrime
                    // which means they should be functions if the runtime is a function
                    importAliasInfo.push(`${exportName}: TS import/export declaration (${kindName})`);
                    isTsFunction = isRuntimeFunction; // Assume correctness for import aliases
                }

                // Handle namespace imports (export import syntax creates these)
                else if (kind === tsMorph.SyntaxKind.NamespaceImport) {
                    importAliasInfo.push(`${exportName}: TS namespace import`);
                    isTsFunction = isRuntimeFunction; // Assume correctness for namespace imports
                }
            }

            // Only flag as error if we have confidence in our TS function detection
            // and there's a clear mismatch
            if (
                isRuntimeFunction &&
                !isTsFunction &&
                declarationWithKind.getKind &&
                declarationWithKind.getKind() !== tsMorph.SyntaxKind.ImportEqualsDeclaration &&
                declarationWithKind.getKind() !== tsMorph.SyntaxKind.ImportClause &&
                declarationWithKind.getKind() !== tsMorph.SyntaxKind.ImportDeclaration &&
                declarationWithKind.getKind() !== tsMorph.SyntaxKind.ExportDeclaration &&
                declarationWithKind.getKind() !== tsMorph.SyntaxKind.NamespaceImport
            ) {
                typeErrors.push(
                    `${exportName}: Runtime is function but TS declares as non-function (kind: ${declarationWithKind.getKindName?.() || 'unknown'})`
                );
            } else if (
                !isRuntimeFunction &&
                isTsFunction &&
                declarationWithKind.getKind &&
                (declarationWithKind.getKind() === tsMorph.SyntaxKind.FunctionDeclaration ||
                    declarationWithKind.getKind() === tsMorph.SyntaxKind.MethodSignature)
            ) {
                typeErrors.push(`${exportName}: TS declares as function but runtime is ${typeof runtimeValue}`);
            }
        }

        if (importAliasInfo.length > 0) {
            console.log('Import alias declarations found:', importAliasInfo.length);
            console.log('Sample import aliases:', importAliasInfo.slice(0, 5));
        }

        if (typeErrors.length > 0) {
            console.log('Function/Property type mismatches:', typeErrors);
        }

        // We expect very few type errors since most functions are import aliases
        expect(typeErrors.length).toBeLessThan(5);
    });

    it('should have reasonable function arity for declared functions', () => {
        const arityMismatches: string[] = [];

        for (const exportName of tsApiExports) {
            const runtimeValue = nerdamer[exportName];
            const tsDeclarations = tsExportedDeclarations.get(exportName) || [];

            if (typeof runtimeValue !== 'function' || tsDeclarations.length === 0) {
                continue;
            }

            const tsDeclaration = tsDeclarations[0]!;

            // Try to get parameters from TS declaration using type guard
            let tsParameterCount = 0;
            const declarationWithParams = tsDeclaration as { getParameters?: () => tsMorph.ParameterDeclaration[] };
            if (declarationWithParams.getParameters) {
                const params = declarationWithParams.getParameters();
                // Count required parameters (non-optional, non-rest)
                tsParameterCount = params.filter(
                    (p: tsMorph.ParameterDeclaration) => !p.hasQuestionToken() && !p.isRestParameter()
                ).length;
            }

            const runtimeArity = runtimeValue.length;

            // Allow some flexibility for variadic functions and optional parameters
            // Flag only when there's a significant mismatch
            if (runtimeArity > 0 && tsParameterCount > 0 && Math.abs(runtimeArity - tsParameterCount) > 2) {
                arityMismatches.push(
                    `${exportName}: Runtime arity ${runtimeArity} vs TS required params ${tsParameterCount}`
                );
            }
        }

        if (arityMismatches.length > 0) {
            console.log('Significant arity mismatches:', arityMismatches);
        }

        // For now, just log mismatches rather than failing the test
        // since some functions may have complex parameter handling
        expect(arityMismatches.length).toBeLessThan(jsApiExports.length * 0.5); // Allow up to 50% mismatches
    });

    it('should detect potentially problematic runtime functions', () => {
        const issues: string[] = [];

        for (const exportName of jsApiExports) {
            const runtimeValue = nerdamer[exportName];

            if (typeof runtimeValue === 'function') {
                // Check for functions that might throw immediately
                try {
                    // Test if function accepts no arguments without throwing
                    const stringForm = runtimeValue.toString();

                    // Flag functions that seem to require arguments but have arity 0
                    if (runtimeValue.length === 0 && stringForm.includes('arguments[0]')) {
                        issues.push(`${exportName}: Function has arity 0 but accesses arguments[0]`);
                    }

                    // Flag functions with unusual patterns
                    if (stringForm.includes('throw') && stringForm.length < 100) {
                        issues.push(`${exportName}: Function appears to primarily throw errors`);
                    }
                } catch (error) {
                    // Function threw during inspection
                    issues.push(`${exportName}: Function threw during reflection: ${error}`);
                }
            } else if (runtimeValue === undefined) {
                issues.push(`${exportName}: Runtime value is undefined`);
            } else if (runtimeValue === null) {
                issues.push(`${exportName}: Runtime value is null`);
            }
        }

        if (issues.length > 0) {
            console.log('Potential runtime issues:', issues);
        }

        // Log issues but don't fail test - these are informational
        expect(issues.length).toBeLessThan(jsApiExports.length); // Just ensure not everything is broken
    });

    it('should categorize API exports by type', () => {
        const categories = {
            functions: [] as string[],
            objects: [] as string[],
            primitives: [] as string[],
            undefined: [] as string[],
            null: [] as string[],
        };

        for (const exportName of jsApiExports) {
            const runtimeValue = nerdamer[exportName];
            const type = typeof runtimeValue;

            if (type === 'function') {
                categories.functions.push(exportName);
            } else if (type === 'object') {
                if (runtimeValue === null) {
                    categories.null.push(exportName);
                } else {
                    categories.objects.push(exportName);
                }
            } else if (type === 'undefined') {
                categories.undefined.push(exportName);
            } else {
                categories.primitives.push(exportName);
            }
        }

        console.log('\n=== API Export Categories ===');
        console.log('Functions:', categories.functions.length, categories.functions);
        console.log('Objects:', categories.objects.length, categories.objects);
        console.log('Primitives:', categories.primitives.length, categories.primitives);
        console.log('Undefined:', categories.undefined.length, categories.undefined);
        console.log('Null:', categories.null.length, categories.null);

        // Ensure we have a reasonable distribution
        expect(categories.functions.length).toBeGreaterThan(0);
        expect(categories.undefined.length + categories.null.length).toBeLessThan(jsApiExports.length * 0.1);
    });

    it('should validate TypeScript declaration structure', () => {
        const structureIssues: string[] = [];

        for (const exportName of tsApiExports) {
            const tsDeclarations = tsExportedDeclarations.get(exportName) || [];

            if (tsDeclarations.length === 0) {
                structureIssues.push(`${exportName}: No TS declaration found`);
                continue;
            }

            if (tsDeclarations.length > 1) {
                // Multiple declarations might indicate overloads, which is fine
                const kinds = tsDeclarations.map((d: tsMorph.ExportedDeclarations) =>
                    'getKindName' in d && typeof d.getKindName === 'function' ? d.getKindName() : 'Unknown'
                );
                console.log(`${exportName}: Multiple declarations (${kinds.join(', ')})`);
            }

            const tsDeclaration = tsDeclarations[0]!;

            // Check for basic structure issues - use type guard
            const declarationWithKind = tsDeclaration as { getKind?: () => number };
            if (!declarationWithKind.getKind) {
                structureIssues.push(`${exportName}: TS declaration missing getKind method`);
            }
        }

        if (structureIssues.length > 0) {
            console.log('TS declaration structure issues:', structureIssues);
        }

        expect(structureIssues).toEqual([]);
    });

    it('should validate import alias targets in nerdamerPrime namespace', () => {
        // This test checks that import aliases like "export import cos = nerdamerPrime.cos"
        // actually reference functions in the nerdamerPrime namespace
        const aliasValidationErrors: string[] = [];
        const namespaceAnalysis: Record<string, number> = {};

        // Get the nerdamerPrime namespace declarations
        const nerdamerPrimeNamespace = sourceFile
            .getDescendantsOfKind(tsMorph.SyntaxKind.ModuleDeclaration)
            .find((ns: tsMorph.ModuleDeclaration) => ns.getName() === 'nerdamerPrime');

        if (nerdamerPrimeNamespace) {
            const primeExports = nerdamerPrimeNamespace.getExportedDeclarations();

            for (const exportName of tsApiExports) {
                const runtimeValue = nerdamer[exportName];
                const isRuntimeFunction = typeof runtimeValue === 'function';

                // Check if this export has a corresponding declaration in nerdamerPrime
                const primeDeclarations = primeExports.get(exportName) || [];

                if (primeDeclarations.length > 0) {
                    const primeDeclaration = primeDeclarations[0];

                    if (primeDeclaration?.getKind) {
                        const kindName = primeDeclaration.getKindName?.() || 'Unknown';
                        namespaceAnalysis[kindName] = (namespaceAnalysis[kindName] || 0) + 1;

                        if (isRuntimeFunction) {
                            const kind = primeDeclaration.getKind();
                            const isPrimeFunction = kind === tsMorph.SyntaxKind.FunctionDeclaration;

                            if (!isPrimeFunction) {
                                aliasValidationErrors.push(
                                    `${exportName}: Runtime is function but nerdamerPrime declares as ${kindName}`
                                );
                            }
                        }
                    }
                }
            }

            console.log('\n=== nerdamerPrime Namespace Analysis ===');
            console.log('Declaration types in nerdamerPrime:', namespaceAnalysis);

            if (aliasValidationErrors.length > 0) {
                console.log('Import alias validation errors (first 10):', aliasValidationErrors.slice(0, 10));
            }

            // Allow some mismatches since nerdamerPrime might have different declaration patterns
            expect(aliasValidationErrors.length).toBeLessThan(tsApiExports.length * 0.2);
        } else {
            console.log('Warning: nerdamerPrime namespace not found');
            // If namespace not found, just pass the test
            expect(true).toBe(true);
        }
    });

    it('should detect TypeScript declaration patterns and issues', () => {
        // This test analyzes common TypeScript patterns used in the declarations
        const declarationPatterns: Record<string, number> = {};
        const potentialIssues: string[] = [];

        for (const exportName of tsApiExports) {
            const tsDeclarations = tsExportedDeclarations.get(exportName) || [];
            const runtimeValue = nerdamer[exportName];
            const isRuntimeFunction = typeof runtimeValue === 'function';

            if (tsDeclarations.length > 0) {
                const tsDeclaration = tsDeclarations[0]!;

                // Use type guard for declarations that may have getKind
                const declarationWithMethods = tsDeclaration as {
                    getKind?: () => number;
                    getKindName?: () => string;
                };
                if (declarationWithMethods.getKind) {
                    const kindName = declarationWithMethods.getKindName?.() || 'Unknown';
                    declarationPatterns[kindName] = (declarationPatterns[kindName] || 0) + 1;

                    // Look for potential issues
                    if (isRuntimeFunction && kindName.includes('Variable')) {
                        potentialIssues.push(`${exportName}: Function runtime but declared as ${kindName}`);
                    }

                    // Check for missing JSDoc or type annotations
                    const declWithJsDocs = tsDeclaration as { getJsDocs?: () => unknown[] };
                    if (declWithJsDocs.getJsDocs?.().length === 0 && kindName === 'FunctionDeclaration') {
                        // Note: Not flagging this as an error since many functions may not have JSDoc
                    }
                }
            }
        }

        console.log('\n=== TypeScript Declaration Patterns ===');
        console.log('Declaration types used:', declarationPatterns);

        if (potentialIssues.length > 0) {
            console.log('Potential declaration issues:', potentialIssues.slice(0, 5));
        }

        // Since all functions use ImportEqualsDeclaration, we expect exactly 1 type
        expect(Object.keys(declarationPatterns).length).toBe(1);
        expect(potentialIssues.length).toBeLessThan(tsApiExports.length * 0.1);

        // Validate that ImportEqualsDeclaration is the only pattern
        expect(declarationPatterns['ImportEqualsDeclaration']).toBe(182);
    });

    it('should log detailed API surface comparison', () => {
        console.log('\n=== Enhanced API Surface Analysis ===');
        console.log('JavaScript API exports:', jsApiExports.length, 'items');
        console.log('TypeScript API exports:', tsApiExports.length, 'items');

        // Show first few exports for quick verification
        console.log('\nFirst 10 JavaScript exports:', jsApiExports.slice(0, 10));
        console.log('First 10 TypeScript exports:', tsApiExports.slice(0, 10));

        // Show any items that are only in one side
        const onlyInJs = jsApiExports.filter(p => !tsApiExports.includes(p));
        const onlyInTs = tsApiExports.filter(p => !jsApiExports.includes(p));

        if (onlyInJs.length > 0) {
            console.log('\nOnly in JavaScript:', onlyInJs);
        }
        if (onlyInTs.length > 0) {
            console.log('Only in TypeScript:', onlyInTs);
        }

        // Show overlap
        const overlap = jsApiExports.filter(p => tsApiExports.includes(p));
        console.log(
            `\nAPI Surface overlap: ${overlap.length}/${Math.max(jsApiExports.length, tsApiExports.length)} (${Math.round((overlap.length / Math.max(jsApiExports.length, tsApiExports.length)) * 100)}%)`
        );
    });
});
