import { createRule } from '../utils/index.js';
import { ReferenceTracker } from '@eslint-community/eslint-utils';
import { FindVariableContext } from '../utils/ast-utils.js';
import { findVariable } from '../utils/ast-utils.js';
export default createRule('no-navigation-without-resolve', {
    meta: {
        docs: {
            description: 'disallow using navigation (links, goto, pushState, replaceState) without a resolve()',
            category: 'SvelteKit',
            recommended: true
        },
        schema: [
            {
                type: 'object',
                properties: {
                    ignoreGoto: {
                        type: 'boolean'
                    },
                    ignoreLinks: {
                        type: 'boolean'
                    },
                    ignorePushState: {
                        type: 'boolean'
                    },
                    ignoreReplaceState: {
                        type: 'boolean'
                    }
                },
                additionalProperties: false
            }
        ],
        messages: {
            gotoWithoutResolve: 'Unexpected goto() call without resolve().',
            linkWithoutResolve: 'Unexpected href link without resolve().',
            pushStateWithoutResolve: 'Unexpected pushState() call without resolve().',
            replaceStateWithoutResolve: 'Unexpected replaceState() call without resolve().'
        },
        type: 'suggestion',
        conditions: [
            {
                svelteKitVersions: ['1.0.0-next', '1', '2']
            }
        ]
    },
    create(context) {
        let resolveReferences = new Set();
        const ignoreGoto = context.options[0]?.ignoreGoto ?? false;
        const ignorePushState = context.options[0]?.ignorePushState ?? false;
        const ignoreReplaceState = context.options[0]?.ignoreReplaceState ?? false;
        const ignoreLinks = context.options[0]?.ignoreLinks ?? false;
        return {
            Program() {
                const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope);
                resolveReferences = extractResolveReferences(referenceTracker, context);
                const { goto: gotoCalls, pushState: pushStateCalls, replaceState: replaceStateCalls } = extractFunctionCallReferences(referenceTracker);
                if (!ignoreGoto) {
                    for (const gotoCall of gotoCalls) {
                        checkGotoCall(context, gotoCall, resolveReferences);
                    }
                }
                if (!ignorePushState) {
                    for (const pushStateCall of pushStateCalls) {
                        checkShallowNavigationCall(context, pushStateCall, resolveReferences, 'pushStateWithoutResolve');
                    }
                }
                if (!ignoreReplaceState) {
                    for (const replaceStateCall of replaceStateCalls) {
                        checkShallowNavigationCall(context, replaceStateCall, resolveReferences, 'replaceStateWithoutResolve');
                    }
                }
            },
            SvelteShorthandAttribute(node) {
                if (ignoreLinks ||
                    node.parent.parent.type !== 'SvelteElement' ||
                    node.parent.parent.kind !== 'html' ||
                    node.parent.parent.name.type !== 'SvelteName' ||
                    node.parent.parent.name.name !== 'a' ||
                    node.key.name !== 'href' ||
                    node.value.type !== 'Identifier') {
                    return;
                }
                if (!expressionIsAbsolute(new FindVariableContext(context), node.value) &&
                    !expressionIsFragment(new FindVariableContext(context), node.value) &&
                    !isResolveCall(new FindVariableContext(context), node.value, resolveReferences)) {
                    context.report({ loc: node.loc, messageId: 'linkWithoutResolve' });
                }
            },
            SvelteAttribute(node) {
                if (ignoreLinks ||
                    node.parent.parent.type !== 'SvelteElement' ||
                    node.parent.parent.kind !== 'html' ||
                    node.parent.parent.name.type !== 'SvelteName' ||
                    node.parent.parent.name.name !== 'a' ||
                    node.key.name !== 'href') {
                    return;
                }
                if ((node.value[0].type === 'SvelteLiteral' &&
                    !expressionIsNullish(new FindVariableContext(context), node.value[0]) &&
                    !expressionIsAbsolute(new FindVariableContext(context), node.value[0]) &&
                    !expressionIsFragment(new FindVariableContext(context), node.value[0])) ||
                    (node.value[0].type === 'SvelteMustacheTag' &&
                        !expressionIsNullish(new FindVariableContext(context), node.value[0].expression) &&
                        !expressionIsAbsolute(new FindVariableContext(context), node.value[0].expression) &&
                        !expressionIsFragment(new FindVariableContext(context), node.value[0].expression) &&
                        !isResolveCall(new FindVariableContext(context), node.value[0].expression, resolveReferences))) {
                    context.report({ loc: node.value[0].loc, messageId: 'linkWithoutResolve' });
                }
            }
        };
    }
});
// Extract all imports of the resolve() function
function extractResolveReferences(referenceTracker, context) {
    const set = new Set();
    for (const { node } of referenceTracker.iterateEsmReferences({
        '$app/paths': {
            [ReferenceTracker.ESM]: true,
            asset: {
                [ReferenceTracker.READ]: true
            },
            resolve: {
                [ReferenceTracker.READ]: true
            }
        }
    })) {
        if (node.type === 'ImportSpecifier') {
            const variable = findVariable(context, node.local);
            if (variable === null) {
                continue;
            }
            for (const reference of variable.references) {
                if (reference.identifier.type === 'Identifier')
                    set.add(reference.identifier);
            }
        }
        else if (node.type === 'MemberExpression' &&
            node.property.type === 'Identifier' &&
            node.property.name === 'resolve') {
            set.add(node.property);
        }
    }
    return set;
}
// Extract all references to goto, pushState and replaceState
function extractFunctionCallReferences(referenceTracker) {
    const rawReferences = Array.from(referenceTracker.iterateEsmReferences({
        '$app/navigation': {
            [ReferenceTracker.ESM]: true,
            goto: {
                [ReferenceTracker.CALL]: true
            },
            pushState: {
                [ReferenceTracker.CALL]: true
            },
            replaceState: {
                [ReferenceTracker.CALL]: true
            }
        }
    }));
    return {
        goto: rawReferences
            .filter(({ path }) => path[path.length - 1] === 'goto')
            .map(({ node }) => node),
        pushState: rawReferences
            .filter(({ path }) => path[path.length - 1] === 'pushState')
            .map(({ node }) => node),
        replaceState: rawReferences
            .filter(({ path }) => path[path.length - 1] === 'replaceState')
            .map(({ node }) => node)
    };
}
// Actual function checking
function checkGotoCall(context, call, resolveReferences) {
    if (call.arguments.length < 1) {
        return;
    }
    const url = call.arguments[0];
    if (!isResolveCall(new FindVariableContext(context), url, resolveReferences)) {
        context.report({ loc: url.loc, messageId: 'gotoWithoutResolve' });
    }
}
function checkShallowNavigationCall(context, call, resolveReferences, messageId) {
    if (call.arguments.length < 1) {
        return;
    }
    const url = call.arguments[0];
    if (!expressionIsEmpty(url) &&
        !isResolveCall(new FindVariableContext(context), url, resolveReferences)) {
        context.report({ loc: url.loc, messageId });
    }
}
// Helper functions
function isResolveCall(ctx, node, resolveReferences) {
    if (node.type === 'CallExpression' &&
        ((node.callee.type === 'Identifier' && resolveReferences.has(node.callee)) ||
            (node.callee.type === 'MemberExpression' &&
                node.callee.property.type === 'Identifier' &&
                resolveReferences.has(node.callee.property)))) {
        return true;
    }
    if (node.type !== 'Identifier') {
        return false;
    }
    const variable = ctx.findVariable(node);
    if (variable === null ||
        variable.identifiers.length === 0 ||
        variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
        variable.identifiers[0].parent.init === null) {
        return false;
    }
    return isResolveCall(ctx, variable.identifiers[0].parent.init, resolveReferences);
}
function expressionIsEmpty(url) {
    return ((url.type === 'Literal' && url.value === '') ||
        (url.type === 'TemplateLiteral' &&
            url.expressions.length === 0 &&
            url.quasis.length === 1 &&
            url.quasis[0].value.raw === ''));
}
function expressionIsNullish(ctx, url) {
    switch (url.type) {
        case 'Identifier':
            return identifierIsNullish(ctx, url);
        case 'Literal':
            return url.value === null; // Undefined is an Identifier in ESTree, null is a Literal
        default:
            return false;
    }
}
function identifierIsNullish(ctx, url) {
    if (url.name === 'undefined') {
        return true;
    }
    const variable = ctx.findVariable(url);
    if (variable === null ||
        variable.identifiers.length === 0 ||
        variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
        variable.identifiers[0].parent.init === null) {
        return false;
    }
    return expressionIsNullish(ctx, variable.identifiers[0].parent.init);
}
function expressionIsAbsolute(ctx, url) {
    switch (url.type) {
        case 'BinaryExpression':
            return binaryExpressionIsAbsolute(ctx, url);
        case 'Identifier':
            return identifierIsAbsolute(ctx, url);
        case 'Literal':
            return typeof url.value === 'string' && urlValueIsAbsolute(url.value);
        case 'SvelteLiteral':
            return urlValueIsAbsolute(url.value);
        case 'TemplateLiteral':
            return templateLiteralIsAbsolute(ctx, url);
        default:
            return false;
    }
}
function binaryExpressionIsAbsolute(ctx, url) {
    return ((url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(ctx, url.left)) ||
        expressionIsAbsolute(ctx, url.right));
}
function identifierIsAbsolute(ctx, url) {
    const variable = ctx.findVariable(url);
    if (variable === null ||
        variable.identifiers.length === 0 ||
        variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
        variable.identifiers[0].parent.init === null) {
        return false;
    }
    return expressionIsAbsolute(ctx, variable.identifiers[0].parent.init);
}
function templateLiteralIsAbsolute(ctx, url) {
    return (url.expressions.some((expression) => expressionIsAbsolute(ctx, expression)) ||
        url.quasis.some((quasi) => urlValueIsAbsolute(quasi.value.raw)));
}
function urlValueIsAbsolute(url) {
    return /^[+a-z]*:/i.test(url);
}
function expressionIsFragment(ctx, url) {
    switch (url.type) {
        case 'BinaryExpression':
            return binaryExpressionIsFragment(ctx, url);
        case 'Identifier':
            return identifierIsFragment(ctx, url);
        case 'Literal':
            return typeof url.value === 'string' && urlValueIsFragment(url.value);
        case 'SvelteLiteral':
            return urlValueIsFragment(url.value);
        case 'TemplateLiteral':
            return templateLiteralIsFragment(ctx, url);
        default:
            return false;
    }
}
function binaryExpressionIsFragment(ctx, url) {
    return url.left.type !== 'PrivateIdentifier' && expressionIsFragment(ctx, url.left);
}
function identifierIsFragment(ctx, url) {
    const variable = ctx.findVariable(url);
    if (variable === null ||
        variable.identifiers.length === 0 ||
        variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
        variable.identifiers[0].parent.init === null) {
        return false;
    }
    return expressionIsFragment(ctx, variable.identifiers[0].parent.init);
}
function templateLiteralIsFragment(ctx, url) {
    return ((url.expressions.length >= 1 && expressionIsFragment(ctx, url.expressions[0])) ||
        (url.quasis.length >= 1 && urlValueIsFragment(url.quasis[0].value.raw)));
}
function urlValueIsFragment(url) {
    return url.startsWith('#');
}
