// SPDX-License-Identifier: Apache-2.0
import { afterEach, beforeEach, describe, it } from 'mocha';
import { expect } from 'chai';
import fs from 'node:fs';
import { ComponentUpgradeMigrationRules } from '../../../../src/commands/migrations/component-upgrade-rules.js';
import * as constants from '../../../../src/core/constants.js';
import sinon from 'sinon';
describe('ComponentUpgradeMigrationRules.planUpgradeMigrationPath', () => {
    let existsSyncStub;
    let readFileSyncStub;
    beforeEach(() => {
        ComponentUpgradeMigrationRules.resetCache();
        existsSyncStub = sinon.stub(fs, 'existsSync');
        readFileSyncStub = sinon.stub(fs, 'readFileSync');
        existsSyncStub.callThrough();
        readFileSyncStub.callThrough();
    });
    afterEach(() => {
        ComponentUpgradeMigrationRules.resetCache();
        sinon.restore();
    });
    describe('same version (no-op upgrade)', () => {
        it('returns a single in-place step for same version', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.27.0', '0.27.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('in-place');
            expect(steps[0].fromVersion).to.equal('0.27.0');
            expect(steps[0].toVersion).to.equal('0.27.0');
        });
    });
    describe('downgrade (no forward boundary crossing)', () => {
        it('returns a single in-place step when downgrading', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.1', '0.27.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('in-place');
            expect(steps[0].fromVersion).to.equal('0.28.1');
            expect(steps[0].toVersion).to.equal('0.27.0');
        });
    });
    describe('upgrade not crossing a boundary', () => {
        it('returns a single in-place step when already past the 0.28.1 boundary', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.1', '0.29.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('in-place');
            expect(steps[0].fromVersion).to.equal('0.28.1');
            expect(steps[0].toVersion).to.equal('0.29.0');
        });
        it('returns a single in-place step for a larger upgrade already past 0.28.1', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.1', '0.35.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('in-place');
            expect(steps[0].fromVersion).to.equal('0.28.1');
            expect(steps[0].toVersion).to.equal('0.35.0');
        });
    });
    describe('upgrade crossing the 0.28.1 boundary', () => {
        it('returns a single recreate step when upgrading from below to exactly 0.28.1', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.0', '0.28.1');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('0.28.0');
            expect(steps[0].toVersion).to.equal('0.28.1');
        });
        it('returns a single recreate step going directly to target when crossing 0.28.1', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.0', '0.35.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('0.28.0');
            expect(steps[0].toVersion).to.equal('0.35.0');
        });
    });
    describe('upgrade crossing a custom boundary', () => {
        it('returns a single recreate step when upgrading across a custom recreate boundary', () => {
            const customConfig = {
                components: {
                    'block-node': {
                        defaultStrategy: 'in-place',
                        boundaries: [{ version: '1.0.0', strategy: 'recreate', reason: 'Custom boundary' }],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.9.0', '1.0.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('0.9.0');
            expect(steps[0].toVersion).to.equal('1.0.0');
        });
        it('returns a recreate step going directly to target when crossing a custom boundary', () => {
            const customConfig = {
                components: {
                    'block-node': {
                        defaultStrategy: 'in-place',
                        boundaries: [{ version: '1.0.0', strategy: 'recreate', reason: 'Custom boundary' }],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.9.0', '1.5.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('0.9.0');
            expect(steps[0].toVersion).to.equal('1.5.0');
        });
        it('splits into multiple steps when multiple boundaries with different strategies are crossed', () => {
            const customConfig = {
                components: {
                    'multi-boundary': {
                        defaultStrategy: 'in-place',
                        boundaries: [
                            { version: '2.0.0', strategy: 'recreate', reason: 'Immutable field change at 2.0.0' },
                            { version: '3.0.0', strategy: 'in-place', reason: 'Config change at 3.0.0' },
                        ],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('multi-boundary', '1.0.0', '4.0.0');
            expect(steps).to.have.length(2);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('1.0.0');
            expect(steps[0].toVersion).to.equal('2.0.0');
            expect(steps[1].strategy).to.equal('in-place');
            expect(steps[1].fromVersion).to.equal('2.0.0');
            expect(steps[1].toVersion).to.equal('4.0.0');
        });
        it('merges consecutive boundaries with the same strategy into a single step', () => {
            const customConfig = {
                components: {
                    'merge-test': {
                        defaultStrategy: 'in-place',
                        boundaries: [
                            { version: '2.0.0', strategy: 'recreate', reason: 'First recreate' },
                            { version: '3.0.0', strategy: 'recreate', reason: 'Second recreate' },
                        ],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('merge-test', '1.0.0', '4.0.0');
            // Both recreate boundaries should merge into a single step jumping to the target
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('1.0.0');
            expect(steps[0].toVersion).to.equal('4.0.0');
        });
        it('handles three alternating-strategy boundaries correctly', () => {
            const customConfig = {
                components: {
                    alternating: {
                        defaultStrategy: 'in-place',
                        boundaries: [
                            { version: '2.0.0', strategy: 'recreate', reason: 'Recreate at 2.0' },
                            { version: '3.0.0', strategy: 'in-place', reason: 'In-place at 3.0' },
                            { version: '4.0.0', strategy: 'recreate', reason: 'Recreate at 4.0' },
                        ],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('alternating', '1.0.0', '5.0.0');
            // 3 distinct strategy segments: recreate → in-place → recreate
            expect(steps).to.have.length(3);
            expect(steps[0]).to.deep.include({ fromVersion: '1.0.0', toVersion: '2.0.0', strategy: 'recreate' });
            expect(steps[1]).to.deep.include({ fromVersion: '2.0.0', toVersion: '3.0.0', strategy: 'in-place' });
            expect(steps[2]).to.deep.include({ fromVersion: '3.0.0', toVersion: '5.0.0', strategy: 'recreate' });
        });
        it('only crosses boundaries within the upgrade range, not all boundaries', () => {
            const customConfig = {
                components: {
                    'partial-cross': {
                        defaultStrategy: 'in-place',
                        boundaries: [
                            { version: '2.0.0', strategy: 'recreate', reason: 'Recreate at 2.0' },
                            { version: '5.0.0', strategy: 'recreate', reason: 'Recreate at 5.0' },
                        ],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            // Upgrading 1.0→3.0 only crosses 2.0, not 5.0
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('partial-cross', '1.0.0', '3.0.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('1.0.0');
            expect(steps[0].toVersion).to.equal('3.0.0');
        });
    });
    describe('unknown component', () => {
        it('returns a single in-place step with default strategy for unknown component', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('unknown-component', '1.0.0', '2.0.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('in-place');
            expect(steps[0].fromVersion).to.equal('1.0.0');
            expect(steps[0].toVersion).to.equal('2.0.0');
        });
    });
    describe('custom config file override', () => {
        it('loads custom config from file and applies its boundary rules', () => {
            const customConfig = {
                components: {
                    'my-component': {
                        defaultStrategy: 'in-place',
                        boundaries: [
                            {
                                version: '2.0.0',
                                strategy: 'recreate',
                                reason: 'Breaking change at 2.0.0',
                            },
                        ],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('my-component', '1.0.0', '2.0.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('1.0.0');
            expect(steps[0].toVersion).to.equal('2.0.0');
        });
        it('falls back to safe empty config if the file has invalid JSON', () => {
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns('not valid json');
            // Falls back to the safe empty config, so the upgrade uses the default in-place strategy.
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.0', '0.28.1');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('in-place');
        });
        it('falls back to safe empty config if the file is missing the components field', () => {
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify({ notComponents: {} }));
            // Falls back to the safe empty config, so the upgrade uses the default in-place strategy.
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.0', '0.28.1');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('in-place');
        });
    });
    describe('step metadata', () => {
        it('includes reason text in migration steps', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.0', '0.28.1');
            expect(steps[0].reason).to.be.a('string').and.not.equal('');
        });
        it('includes extraCommandArgs array in migration steps', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.0', '0.28.1');
            expect(steps[0].extraCommandArgs).to.be.an('array');
        });
        it('propagates boundary-specific extraCommandArgs to the migration step', () => {
            const customConfig = {
                components: {
                    'args-test': {
                        defaultStrategy: 'in-place',
                        boundaries: [
                            {
                                version: '2.0.0',
                                strategy: 'recreate',
                                reason: 'Breaking change',
                                extraCommandArgs: ['--set', 'migration.enabled=true'],
                            },
                        ],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('args-test', '1.0.0', '3.0.0');
            expect(steps[0].extraCommandArgs).to.deep.equal(['--set', 'migration.enabled=true']);
        });
        it('propagates defaultExtraCommandArgs to default-strategy steps', () => {
            const customConfig = {
                components: {
                    'default-args-test': {
                        defaultStrategy: 'in-place',
                        defaultExtraCommandArgs: ['--timeout', '600s'],
                        boundaries: [],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('default-args-test', '1.0.0', '2.0.0');
            expect(steps[0].extraCommandArgs).to.deep.equal(['--timeout', '600s']);
        });
    });
    describe('version normalization', () => {
        it('handles versions with v prefix', () => {
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', 'v0.28.0', 'v0.28.1');
            // v0.28.0→v0.28.1 crosses the 0.28.1 recreate boundary after stripping the 'v' prefix
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].fromVersion).to.equal('0.28.0');
            expect(steps[0].toVersion).to.equal('0.28.1');
        });
        it('handles pre-release versions correctly', () => {
            // 0.28.1-rc.1 < 0.28.1 in semver, so the boundary at 0.28.1 is NOT crossed
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.0', '0.28.1-rc.1');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('in-place');
        });
    });
    describe('default strategy override', () => {
        it('uses recreate as default strategy when configured', () => {
            const customConfig = {
                components: {
                    'recreate-default': {
                        defaultStrategy: 'recreate',
                        boundaries: [],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('recreate-default', '1.0.0', '2.0.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
        });
        it('uses recreate default strategy for downgrade when configured', () => {
            const customConfig = {
                components: {
                    'recreate-default': {
                        defaultStrategy: 'recreate',
                        boundaries: [],
                    },
                },
            };
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(true);
            readFileSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE, 'utf8').returns(JSON.stringify(customConfig));
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('recreate-default', '2.0.0', '1.0.0');
            expect(steps).to.have.length(1);
            expect(steps[0].strategy).to.equal('recreate');
            expect(steps[0].reason).to.equal('No forward upgrade boundary crossing detected');
        });
    });
    describe('config caching', () => {
        it('caches config after first load and does not re-read file', () => {
            existsSyncStub.withArgs(constants.UPGRADE_MIGRATIONS_FILE).returns(false);
            // First call loads and caches the default config
            ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.27.0', '0.28.0');
            // Second call should use cached config
            ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.27.0', '0.28.0');
            // existsSync should only be called once for the migration file
            const migrationFileCalls = existsSyncStub
                .getCalls()
                .filter((call) => call.args[0] === constants.UPGRADE_MIGRATIONS_FILE);
            expect(migrationFileCalls).to.have.length(1);
        });
    });
    describe('downgrade across boundary', () => {
        it('does not apply boundary rules when downgrading across a boundary version', () => {
            // Downgrading from 0.28.1 to 0.26.0 crosses the 0.28.0 boundary backwards,
            // but boundaries only apply to forward upgrades
            const steps = ComponentUpgradeMigrationRules.planUpgradeMigrationPath('block-node', '0.28.1', '0.26.0');
            expect(steps).to.have.length(1);
            // Should use default strategy, NOT recreate from the boundary
            expect(steps[0].strategy).to.equal('in-place');
            expect(steps[0].reason).to.equal('No forward upgrade boundary crossing detected');
        });
    });
});
//# sourceMappingURL=component-upgrade-rules.test.js.map