'use strict';

var fs = require('fs');
var path = require('path');
var test = require('tape');
var resolve = require('../');

var fixturesDir = path.join(__dirname, 'list-exports', 'packages', 'tests', 'fixtures');

var categories = [
    'broken',
    'broken-dir-slash-conditions',
    'conditions',
    'experimental',
    'pattern-trailers',
    'pattern-trailers+json-imports',
    'pattern-trailers-no-dir-slash',
    'pattern-trailers-no-dir-slash+json-imports',
    'patterns',
    'require-esm',
    'strips-types',
    'subpath-imports-slash'
    // 'pre-exports' is tested separately since it uses main/index resolution
];

// Fixtures that are symlinks pointing outside the fixture dir cause path confusion
// ex-private is a private package whose expected files don't include exports data
var skipFixtures = ['list-exports', 'ls-exports', 'ex-private'];

function getFixtures() {
    return fs.readdirSync(fixturesDir).filter(function (name) {
        if (skipFixtures.indexOf(name) !== -1) {
            return false;
        }
        var stat = fs.statSync(path.join(fixturesDir, name));
        return stat.isDirectory();
    });
}

function loadExpected(fixtureName, category) {
    var expectedPath = path.join(fixturesDir, fixtureName, 'expected', category + '.json');
    if (!fs.existsSync(expectedPath)) {
        return null;
    }
    try {
        return JSON.parse(fs.readFileSync(expectedPath, 'utf8'));
    } catch (e) {
        return null;
    }
}

function loadProjectPkg(fixtureName) {
    var pkgPath = path.join(fixturesDir, fixtureName, 'project', 'package.json');
    try {
        return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
    } catch (e) {
        return null;
    }
}

test('async exports resolution - exportsCategory option', function (t) {
    var fixtures = getFixtures();

    fixtures.forEach(function (fixtureName) {
        var projectPkg = loadProjectPkg(fixtureName);
        if (!projectPkg) {
            return;
        }
        var projectDir = path.join(fixturesDir, fixtureName, 'project');
        var pkgName = projectPkg.name;

        categories.forEach(function (category) {
            var expected = loadExpected(fixtureName, category);
            if (!expected || !expected.exports || !expected.exports[category]) {
                return;
            }

            var requireMap = expected.exports[category].require;
            if (!requireMap || typeof requireMap !== 'object') {
                return;
            }

            Object.keys(requireMap).forEach(function (subpath) {
                var expectedFile = requireMap[subpath];
                var specifier = subpath === '.' ? pkgName : pkgName + subpath.substring(1);

                t.test(fixtureName + ' / ' + category + ' / ' + subpath, function (st) {
                    st.plan(1);
                    resolve(specifier, {
                        basedir: __dirname,
                        exportsCategory: category,
                        extensions: ['.js', '.json'],
                        packageIterator: function () {
                            return [projectDir];
                        }
                    }, function (err, result) {
                        if (err) {
                            st.fail('Unexpected error for ' + specifier + ': ' + err.message);
                            return;
                        }
                        var relativeResult = './' + path.relative(projectDir, result).split(path.sep).join('/');
                        st.equal(relativeResult, expectedFile, specifier + ' resolves to ' + expectedFile);
                    });
                });
            });
        });
    });

    t.end();
});

test('async exports resolution - pre-exports category uses main/index', function (t) {
    var fixtures = getFixtures();

    fixtures.forEach(function (fixtureName) {
        var projectPkg = loadProjectPkg(fixtureName);
        if (!projectPkg) {
            return;
        }
        var projectDir = path.join(fixturesDir, fixtureName, 'project');
        var pkgName = projectPkg.name;

        var expected = loadExpected(fixtureName, 'pre-exports');
        if (!expected || !expected.exports || !expected.exports['pre-exports']) {
            return;
        }

        var requireMap = expected.exports['pre-exports'].require;
        if (!requireMap || typeof requireMap !== 'object') {
            return;
        }

        // For pre-exports, only test the main entry point (.)
        var mainEntry = requireMap['.'];
        if (!mainEntry) {
            return;
        }

        t.test(fixtureName + ' / pre-exports / .', function (st) {
            st.plan(1);
            resolve(pkgName, {
                basedir: __dirname,
                exportsCategory: 'pre-exports',
                extensions: ['.js', '.json'],
                packageIterator: function () {
                    return [projectDir];
                }
            }, function (err, result) {
                if (err) {
                    st.fail('Unexpected error for ' + pkgName + ': ' + err.message);
                    return;
                }
                var relativeResult = './' + path.relative(projectDir, result).split(path.sep).join('/');
                st.equal(relativeResult, mainEntry, pkgName + ' resolves to ' + mainEntry);
            });
        });
    });

    t.end();
});

test('async exports resolution - mutual exclusivity of options', function (t) {
    t.test('exportsCategory and engines (string) are mutually exclusive', function (st) {
        st.plan(1);
        resolve('tape', {
            basedir: __dirname,
            exportsCategory: 'conditions',
            engines: '>= 14'
        }, function (err) {
            st.ok(err && (/mutually exclusive/).test(err.message), 'throws with mutually exclusive message');
        });
    });

    t.test('exportsCategory and engines (true) are mutually exclusive', function (st) {
        st.plan(1);
        resolve('tape', {
            basedir: __dirname,
            exportsCategory: 'conditions',
            engines: true
        }, function (err) {
            st.ok(err && (/mutually exclusive/).test(err.message), 'throws with mutually exclusive message');
        });
    });

    t.end();
});

test('async exports resolution - invalid category', function (t) {
    t.plan(2);
    resolve('tape', {
        basedir: __dirname,
        exportsCategory: 'not-a-real-category'
    }, function (err) {
        t.equal(err && err.code, 'INVALID_EXPORTS_CATEGORY', 'has correct error code');
        t.ok(err && (/Invalid exports category/).test(err.message), 'has correct error message');
    });
});

test('async exports resolution - engines option', function (t) {
    var projectDir = path.join(fixturesDir, 'ex-exports-string', 'project');

    t.test('engines string maps to category', function (st) {
        st.plan(1);
        resolve('ex-exports-string', {
            basedir: __dirname,
            engines: '>= 14',
            packageIterator: function () {
                return [projectDir];
            }
        }, function (err, result) {
            if (err) {
                st.fail(err.message);
                return;
            }
            st.ok(result.indexOf('index.js') > -1, 'resolves to index.js');
        });
    });

    t.test('engines: false is same as omitting', function (st) {
        st.plan(1);
        resolve('tape', {
            basedir: __dirname,
            engines: false
        }, function (err, result) {
            if (err) {
                st.fail(err.message);
                return;
            }
            st.ok(result.indexOf('tape') > -1, 'resolves without exports resolution');
        });
    });

    t.test('engines: empty string throws', function (st) {
        st.plan(1);
        resolve('tape', {
            basedir: __dirname,
            engines: ''
        }, function (err) {
            st.ok(err && (/must be.*true.*false.*non-empty string/i).test(err.message), 'throws with correct message');
        });
    });

    t.test('engines: number throws', function (st) {
        st.plan(1);
        resolve('tape', {
            basedir: __dirname,
            engines: 14
        }, function (err) {
            st.ok(err && (/must be.*true.*false.*non-empty string/i).test(err.message), 'throws with correct message');
        });
    });

    t.test('engines: object throws', function (st) {
        st.plan(1);
        resolve('tape', {
            basedir: __dirname,
            engines: { node: '>= 14' }
        }, function (err) {
            st.ok(err && (/must be.*true.*false.*non-empty string/i).test(err.message), 'throws with correct message');
        });
    });

    t.end();
});

test('async exports resolution - conditions override', function (t) {
    var projectDir = path.join(fixturesDir, 'ex-conditions', 'project');

    t.test('default category conditions resolve to require.js', function (st) {
        st.plan(1);
        resolve('ex-conditions/rdni', {
            basedir: __dirname,
            exportsCategory: 'conditions',
            packageIterator: function () {
                return [projectDir];
            }
        }, function (err, result) {
            if (err) {
                st.fail(err.message);
                return;
            }
            st.ok(result.indexOf('require.js') > -1, 'resolves to require.js with default conditions');
        });
    });

    t.test('conditions override to [default] resolves to default.js', function (st) {
        st.plan(1);
        resolve('ex-conditions/rdni', {
            basedir: __dirname,
            exportsCategory: 'conditions',
            conditions: ['default'],
            packageIterator: function () {
                return [projectDir];
            }
        }, function (err, result) {
            if (err) {
                st.fail(err.message);
                return;
            }
            st.ok(result.indexOf('default.js') > -1, 'resolves to default.js with conditions override');
        });
    });

    t.test('conditions override to [node] resolves to node.js', function (st) {
        st.plan(1);
        resolve('ex-conditions/rdni', {
            basedir: __dirname,
            exportsCategory: 'conditions',
            conditions: ['node'],
            packageIterator: function () {
                return [projectDir];
            }
        }, function (err, result) {
            if (err) {
                st.fail(err.message);
                return;
            }
            st.ok(result.indexOf('node.js') > -1, 'resolves to node.js with conditions override');
        });
    });

    t.end();
});

test('async exports resolution - subpath not exported throws', function (t) {
    var projectDir = path.join(fixturesDir, 'ex-exports-string', 'project');

    t.plan(2);
    resolve('ex-exports-string/not-exported', {
        basedir: __dirname,
        exportsCategory: 'conditions',
        packageIterator: function () {
            return [projectDir];
        }
    }, function (err) {
        t.equal(err && err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED', 'has correct error code');
        t.ok(err && (/not defined by "exports"/).test(err.message), 'has correct error message');
    });
});

test('async existing resolution without exports options still works', function (t) {
    t.plan(1);
    resolve('tape', { basedir: __dirname }, function (err, result) {
        if (err) {
            t.fail(err.message);
            return;
        }
        t.ok(result.indexOf('tape') > -1, 'resolves tape without exports options');
    });
});

test('all fixtures are tested', function (t) {
    var fixtures = getFixtures();
    var testedFixtures = [];

    fixtures.forEach(function (fixtureName) {
        var projectPkg = loadProjectPkg(fixtureName);
        if (!projectPkg) {
            t.fail('Fixture ' + fixtureName + ' has no loadable package.json');
            return;
        }

        var hasAnyTests = false;

        // Check if at least one category has expected results
        categories.forEach(function (category) {
            var expected = loadExpected(fixtureName, category);
            if (expected && expected.exports && expected.exports[category]) {
                var requireMap = expected.exports[category].require;
                if (requireMap && typeof requireMap === 'object' && Object.keys(requireMap).length > 0) {
                    hasAnyTests = true;
                }
            }
        });

        // Also check pre-exports
        var preExpected = loadExpected(fixtureName, 'pre-exports');
        if (preExpected && preExpected.exports && preExpected.exports['pre-exports']) {
            var preRequireMap = preExpected.exports['pre-exports'].require;
            if (preRequireMap && preRequireMap['.']) {
                hasAnyTests = true;
            }
        }

        if (hasAnyTests) {
            testedFixtures.push(fixtureName);
        } else {
            t.fail('Fixture ' + fixtureName + ' has no testable entrypoints');
        }
    });

    t.ok(testedFixtures.length > 0, 'At least one fixture is tested');
    t.equal(testedFixtures.length, fixtures.length, 'All ' + fixtures.length + ' fixtures have testable entrypoints');

    t.end();
});

test('async exports resolution - self-reference', function (t) {
    var projectDir = path.join(fixturesDir, 'ex-exports-string', 'project');

    t.test('self-reference resolves via exports when inside package', function (st) {
        st.plan(1);
        // basedir is inside the package, specifier is the package name
        resolve('ex-exports-string', {
            basedir: projectDir,
            exportsCategory: 'conditions'
        }, function (err, result) {
            if (err) {
                st.fail(err.message);
                return;
            }
            st.ok(result.indexOf('index.js') > -1, 'self-reference resolves to index.js via exports');
        });
    });

    t.test('self-reference with subpath resolves via exports', function (st) {
        var conditionsDir = path.join(fixturesDir, 'ex-conditions', 'project');
        st.plan(1);
        resolve('ex-conditions/rdni', {
            basedir: conditionsDir,
            exportsCategory: 'conditions'
        }, function (err, result) {
            if (err) {
                st.fail(err.message);
                return;
            }
            st.ok(result.indexOf('require.js') > -1, 'self-reference subpath resolves correctly');
        });
    });

    t.test('self-reference without exports falls back to main', function (st) {
        var mainDotlessDir = path.join(fixturesDir, 'ex-main-dotless', 'project');
        st.plan(1);
        resolve('ex-main-dotless', {
            basedir: mainDotlessDir,
            exportsCategory: 'conditions'
        }, function (err, result) {
            // If it throws, that's also acceptable behavior (no exports means not exported)
            st.ok(!err || result, 'self-reference without exports throws or resolves');
        });
    });

    t.test('self-reference does not cross node_modules boundary', function (st) {
        // basedir is inside node_modules, should NOT self-reference parent package
        var nodeModulesDir = path.join(__dirname, '..', 'node_modules', 'tape');
        st.plan(1);
        resolve('resolve', {
            basedir: nodeModulesDir,
            exportsCategory: 'conditions'
        }, function (err, result) {
            if (err) {
                st.fail(err.message);
                return;
            }
            // The result should be from node_modules, not from a self-reference
            st.ok(result.indexOf('node_modules') > -1 || result === 'resolve', 'does not self-reference across node_modules');
        });
    });

    t.end();
});
