import { describe, it, expect } from 'vitest';

// genParamsMapper is pure logic with zero dependencies — safe to import directly
import {
    SAMPLER_DISPLAY_TO_INTERNAL,
    SCHEDULER_SUFFIXES,
    SCHEDULER_ONLY_VALUES,
    PARAM_TO_WIDGET_CANDIDATES,
    parseCombinedSamplerName,
    resolveSamplerScheduler,
    findMatchingWidgets,
    NODE_TYPE_WIDGET_OVERRIDES,
} from '../../../static/js/utils/genParamsMapper.js';

// ---------------------------------------------------------------------------
// Constants sanity
// ---------------------------------------------------------------------------
describe('constants', () => {
    it('maps at least the common samplers', () => {
        expect(SAMPLER_DISPLAY_TO_INTERNAL['Euler']).toBe('euler');
        expect(SAMPLER_DISPLAY_TO_INTERNAL['Euler a']).toBe('euler_ancestral');
        expect(SAMPLER_DISPLAY_TO_INTERNAL['DPM++ 2M']).toBe('dpmpp_2m');
        expect(SAMPLER_DISPLAY_TO_INTERNAL['DPM++ 2M SDE']).toBe('dpmpp_2m_sde');
        expect(SAMPLER_DISPLAY_TO_INTERNAL['LCM']).toBe('lcm');
        expect(SAMPLER_DISPLAY_TO_INTERNAL['DDIM']).toBe('ddim');
    });

    it('lists all 9 scheduler suffixes', () => {
        expect(SCHEDULER_SUFFIXES).toHaveLength(9);
        expect(SCHEDULER_SUFFIXES).toContain('karras');
        expect(SCHEDULER_SUFFIXES).toContain('simple');
        expect(SCHEDULER_SUFFIXES).toContain('exponential');
    });

    it('marks scheduler-only values', () => {
        expect(SCHEDULER_ONLY_VALUES.has('karras')).toBe(true);
        expect(SCHEDULER_ONLY_VALUES.has('simple')).toBe(true);
        expect(SCHEDULER_ONLY_VALUES.has('euler')).toBe(false);
    });

    it('has widget candidates for all param keys', () => {
        expect(PARAM_TO_WIDGET_CANDIDATES.seed).toContain('seed');
        expect(PARAM_TO_WIDGET_CANDIDATES.sampler).toContain('sampler_name');
        expect(PARAM_TO_WIDGET_CANDIDATES.scheduler).toContain('scheduler');
    });
});

// ---------------------------------------------------------------------------
// parseCombinedSamplerName
// ---------------------------------------------------------------------------
describe('parseCombinedSamplerName', () => {
    it('parses space-separated sampler + scheduler', () => {
        expect(parseCombinedSamplerName('Euler a Karras')).toEqual({
            sampler: 'euler_ancestral',
            scheduler: 'karras',
        });
    });

    it('parses DPM++ 2M Karras', () => {
        expect(parseCombinedSamplerName('DPM++ 2M Karras')).toEqual({
            sampler: 'dpmpp_2m',
            scheduler: 'karras',
        });
    });

    it('parses DPM++ 2M beta', () => {
        expect(parseCombinedSamplerName('DPM++ 2M beta')).toEqual({
            sampler: 'dpmpp_2m',
            scheduler: 'beta',
        });
    });

    it('parses DPM++ SDE Karras', () => {
        expect(parseCombinedSamplerName('DPM++ SDE Karras')).toEqual({
            sampler: 'dpmpp_sde',
            scheduler: 'karras',
        });
    });

    it('parses underscore-separated er_sde_beta', () => {
        expect(parseCombinedSamplerName('er_sde_beta')).toEqual({
            sampler: 'er_sde',
            scheduler: 'beta',
        });
    });

    it('returns null for sampler-only values', () => {
        expect(parseCombinedSamplerName('Euler a')).toBeNull();
        expect(parseCombinedSamplerName('LCM')).toBeNull();
    });

    it('returns null for unrecognised suffix', () => {
        expect(parseCombinedSamplerName('Euler something_unknown')).toBeNull();
    });

    it('returns null for null/empty', () => {
        expect(parseCombinedSamplerName(null)).toBeNull();
        expect(parseCombinedSamplerName('')).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// resolveSamplerScheduler — the main resolver used by the send feature
// ---------------------------------------------------------------------------
describe('resolveSamplerScheduler', () => {
    // --- Category 1: simple display names ---
    it('resolves Euler → euler', () => {
        expect(resolveSamplerScheduler('Euler')).toEqual({ sampler: 'euler', scheduler: null });
    });

    it('resolves Euler a → euler_ancestral', () => {
        expect(resolveSamplerScheduler('Euler a')).toEqual({ sampler: 'euler_ancestral', scheduler: null });
    });

    it('resolves DPM++ 2M → dpmpp_2m', () => {
        expect(resolveSamplerScheduler('DPM++ 2M')).toEqual({ sampler: 'dpmpp_2m', scheduler: null });
    });

    it('resolves LCM → lcm', () => {
        expect(resolveSamplerScheduler('LCM')).toEqual({ sampler: 'lcm', scheduler: null });
    });

    // --- Category 2: already-internal names ---
    it('passes through lowercase internal names', () => {
        expect(resolveSamplerScheduler('euler')).toEqual({ sampler: 'euler', scheduler: null });
        expect(resolveSamplerScheduler('heunpp2')).toEqual({ sampler: 'heunpp2', scheduler: null });
        expect(resolveSamplerScheduler('lcm')).toEqual({ sampler: 'lcm', scheduler: null });
        expect(resolveSamplerScheduler('er_sde')).toEqual({ sampler: 'er_sde', scheduler: null });
    });

    // --- Category 3: combined names ---
    it('resolves Euler a Karras → euler_ancestral + karras', () => {
        expect(resolveSamplerScheduler('Euler a Karras')).toEqual({
            sampler: 'euler_ancestral',
            scheduler: 'karras',
        });
    });

    it('resolves DPM++ 2M Karras → dpmpp_2m + karras', () => {
        expect(resolveSamplerScheduler('DPM++ 2M Karras')).toEqual({
            sampler: 'dpmpp_2m',
            scheduler: 'karras',
        });
    });

    // --- Category 4: scheduler-only ---
    it('resolves scheduler-only values', () => {
        expect(resolveSamplerScheduler('karras')).toEqual({ sampler: null, scheduler: 'karras' });
        expect(resolveSamplerScheduler('simple')).toEqual({ sampler: null, scheduler: 'simple' });
        expect(resolveSamplerScheduler('sgm_uniform')).toEqual({ sampler: null, scheduler: 'sgm_uniform' });
    });

    // --- Category 5: unrecognised / model-specific ---
    it('returns null+null for unrecognised values', () => {
        const result = resolveSamplerScheduler('AYS SDXL');
        expect(result.sampler).toBeNull();
        expect(result.scheduler).toBeNull();
    });

    it('returns null+null for Undefined', () => {
        const result = resolveSamplerScheduler('Undefined');
        expect(result.sampler).toBeNull();
        expect(result.scheduler).toBeNull();
    });

    it('returns null+null for model-specific values', () => {
        expect(resolveSamplerScheduler('Seedream-V45').sampler).toBeNull();
        expect(resolveSamplerScheduler('GPT-Image-2').sampler).toBeNull();
    });

    // --- Category 6: edge cases ---
    it('returns null+null for null / empty / whitespace', () => {
        expect(resolveSamplerScheduler(null)).toEqual({ sampler: null, scheduler: null });
        expect(resolveSamplerScheduler('')).toEqual({ sampler: null, scheduler: null });
        expect(resolveSamplerScheduler('   ')).toEqual({ sampler: null, scheduler: null });
    });

    it('handles slash-separated custom format (extracts last segment)', () => {
        // "multistep/dpmpp_2m_simple" — extracts last segment but the recursive
        // call hits the "already internal name" regex before combined-name parsing,
        // so it returns the raw segment as the sampler name.
        const result = resolveSamplerScheduler('multistep/dpmpp_2m_simple');
        expect(result.sampler).toBe('dpmpp_2m_simple');
        expect(result.scheduler).toBeNull();
    });

    it('handles parse-error value (None', () => {
        const result = resolveSamplerScheduler('(None');
        expect(result.sampler).toBeNull();
        expect(result.scheduler).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// findMatchingWidgets
// ---------------------------------------------------------------------------
describe('findMatchingWidgets', () => {
    const resolved = {
        seed: 42,
        steps: 30,
        cfg: 7,
        sampler: 'euler_ancestral',
        scheduler: 'karras',
    };

    it('matches seed to seed widget', () => {
        const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
        expect(updates).toContainEqual({ widgetName: 'seed', value: 42 });
        expect(updates).toContainEqual({ widgetName: 'steps', value: 30 });
        expect(updates).toContainEqual({ widgetName: 'cfg', value: 7 });
        expect(updates).toContainEqual({ widgetName: 'sampler_name', value: 'euler_ancestral' });
        expect(updates).toContainEqual({ widgetName: 'scheduler', value: 'karras' });
    });

    it('skips undefined/null params', () => {
        const updates = findMatchingWidgets(['seed', 'steps'], { seed: 42, steps: null, cfg: undefined });
        expect(updates).toHaveLength(1);
        expect(updates[0].widgetName).toBe('seed');
    });

    it('matches noise_seed when seed widget not present', () => {
        const updates = findMatchingWidgets(['noise_seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
        const seedUpdate = updates.find(u => u.widgetName === 'noise_seed');
        expect(seedUpdate).toBeDefined();
        expect(seedUpdate.value).toBe(42);
    });

    it('matches rgthree-style sampler widget name', () => {
        const updates = findMatchingWidgets(['sampler', 'scheduler'], { sampler: 'euler', scheduler: 'karras' });
        expect(updates).toContainEqual({ widgetName: 'sampler', value: 'euler' });
    });

    it('returns empty array for empty widget list', () => {
        expect(findMatchingWidgets([], resolved)).toEqual([]);
        expect(findMatchingWidgets(null, resolved)).toEqual([]);
    });

    it('handles case-insensitive widget name matching', () => {
        const updates = findMatchingWidgets(['SEED', 'STEPS', 'CFG'], resolved);
        expect(updates).toHaveLength(3);
    });

    it('returns updates in param order (seed, steps, cfg, sampler, scheduler)', () => {
        const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
        expect(updates.map(u => u.widgetName)).toEqual(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler']);
    });

    // --- node-type-specific overrides ---
    it('matches GlobalSeed //Inspire value widget for seed param', () => {
        const updates = findMatchingWidgets(
            ['value', 'mode', 'action', 'last_seed'],
            { seed: 42 },
            'GlobalSeed //Inspire'
        );
        expect(updates).toHaveLength(1);
        expect(updates[0]).toEqual({ widgetName: 'value', value: 42 });
    });

    it('ignores nodeType when it does not match any override entry', () => {
        const updates = findMatchingWidgets(
            ['value', 'mode', 'action', 'last_seed'],
            { seed: 42 },
            'SomeOtherNode'
        );
        expect(updates).toEqual([]);
    });

    it('still falls back to global candidates when override candidates do not match', () => {
        // GlobalSeed override does not include steps — should use global candidate "steps"
        const updates = findMatchingWidgets(
            ['steps', 'cfg', 'sampler_name'],
            { steps: 20 },
            'GlobalSeed //Inspire'
        );
        expect(updates).toHaveLength(1);
        expect(updates[0]).toEqual({ widgetName: 'steps', value: 20 });
    });

    it('prefers overrides when both override and global candidates match', () => {
        // If a hypothetical node has both "value" and "seed" widgets AND a
        // GlobalSeed override, the override candidate "value" should take precedence
        const updates = findMatchingWidgets(
            ['seed', 'noise_seed', 'value', 'mode'],
            { seed: 99 },
            'GlobalSeed //Inspire'
        );
        expect(updates).toHaveLength(1);
        expect(updates[0].widgetName).toBe('value');
    });

    it('omits nodeType argument and still matches via global candidates', () => {
        const updates = findMatchingWidgets(['seed', 'steps', 'cfg'], { seed: 7 });
        expect(updates).toHaveLength(1);
        expect(updates[0]).toEqual({ widgetName: 'seed', value: 7 });
    });
});
