// ==========================================
// Model Processing Tests
// ==========================================
// Tests the data transforms in fetchModels()
// that convert backend responses into the
// App.models array used throughout the UI.
//
// Run: node --test src/js/__tests__/model-processing.test.js
// ==========================================

const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { processModelsResponse } = require('./helpers');

// ── Basic model transform ────────────────────

describe('processModelsResponse — catalog models', () => {
    // Mock uses BOTH canonical (provider_config_id) and alias (config_id)
    // to match the actual UserModel Go struct serialization.
    const apiResponse = {
        models: [
            {
                id: 'entry-uuid',
                model_id: 'gpt-4o',
                display_name: 'GPT-4o',
                provider_config_id: 'cfg-uuid',
                config_id: 'cfg-uuid',
                provider_name: 'OpenAI Production',
                provider_type: 'openai',
                source: 'catalog',
                scope: 'global',
                capabilities: { streaming: true, vision: true, tool_calling: true },
            },
        ],
    };

    it('produces composite ID for catalog models', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].id, 'cfg-uuid:gpt-4o',
            'catalog model ID must be config_id:model_id to avoid collisions');
    });

    it('produces composite ID with ONLY provider_config_id (no config_id alias)', () => {
        // This is the critical regression test: if the config_id alias
        // is removed from the Go struct, the frontend must still work.
        const resp = {
            models: [{
                model_id: 'gpt-4o',
                provider_config_id: 'cfg-uuid',
                // NO config_id — simulates broken alias
                provider_name: 'OpenAI',
                source: 'catalog',
            }],
        };
        const models = processModelsResponse(resp);
        assert.equal(models[0].id, 'cfg-uuid:gpt-4o',
            'must fall back to provider_config_id when config_id is missing');
    });

    it('preserves baseModelId', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].baseModelId, 'gpt-4o');
    });

    it('uses display_name for name', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].name, 'GPT-4o');
    });

    it('falls back to model_id when display_name empty', () => {
        const resp = {
            models: [{ model_id: 'claude-3-opus', provider_config_id: 'c1', provider_type: 'anthropic' }],
        };
        const models = processModelsResponse(resp);
        assert.equal(models[0].name, 'claude-3-opus');
    });

    it('marks catalog models as NOT personas', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].isPersona, false);
    });

    it('preserves configId from config_id || provider_config_id', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].configId, 'cfg-uuid');
    });

    it('uses provider_name for display', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].provider, 'OpenAI Production');
    });
});

// ── Preset transform ─────────────────────────

describe('processModelsResponse — personas', () => {
    const apiResponse = {
        models: [
            {
                id: 'preset-uuid',
                model_id: 'gpt-4o',
                display_name: 'Code Helper',
                // Backend canonical fields
                provider_config_id: 'cfg-uuid',
                persona_id: 'preset-uuid',
                scope: 'global',
                avatar: '/avatars/code.png',
                source: 'persona',
                // Frontend alias fields
                is_persona: true,
                persona_id: 'preset-uuid',
                persona_scope: 'global',
                persona_avatar: '/avatars/code.png',
                persona_handle: 'code-helper',
                persona_team_name: null,
                config_id: 'cfg-uuid',
                provider_name: 'OpenAI',
            },
        ],
    };

    it('uses persona_id as ID (not composite)', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].id, 'preset-uuid',
            'persona ID must use persona_id, not config_id:model_id');
    });

    it('falls back to persona_id when not present', () => {
        const resp = {
            models: [{
                id: 'p-uuid', model_id: 'gpt-4o',
                is_persona: true, persona_id: 'p-uuid',
                // NO preset_id — simulates broken alias
                source: 'persona',
            }],
        };
        const models = processModelsResponse(resp);
        assert.equal(models[0].id, 'p-uuid',
            'must fall back to persona_id');
    });

    it('marks as persona', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].isPersona, true);
    });

    it('preserves persona metadata', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].personaScope, 'global');
        assert.equal(models[0].personaAvatar, '/avatars/code.png');
        assert.equal(models[0].personaHandle, 'code-helper');
        assert.equal(models[0].personaId, 'preset-uuid');
    });

    it('baseModelId is the underlying model', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models[0].baseModelId, 'gpt-4o');
    });
});

// ── Same model from multiple providers ───────

describe('processModelsResponse — multi-provider dedup', () => {
    const apiResponse = {
        models: [
            {
                model_id: 'gpt-4o', display_name: 'GPT-4o',
                provider_config_id: 'global-cfg', config_id: 'global-cfg',
                provider_name: 'Global OpenAI',
                source: 'catalog', scope: 'global',
            },
            {
                model_id: 'gpt-4o', display_name: 'GPT-4o (BYOK)',
                provider_config_id: 'personal-cfg', config_id: 'personal-cfg',
                provider_name: 'My Key',
                source: 'catalog', scope: 'personal',
            },
        ],
    };

    it('same model_id with different config_ids produces unique IDs', () => {
        const models = processModelsResponse(apiResponse);
        assert.equal(models.length, 2);
        assert.notEqual(models[0].id, models[1].id,
            'same model from different providers MUST have unique IDs');
        assert.equal(models[0].id, 'global-cfg:gpt-4o');
        assert.equal(models[1].id, 'personal-cfg:gpt-4o');
    });

    it('works with only provider_config_id (no config_id alias)', () => {
        const resp = {
            models: [
                { model_id: 'gpt-4o', provider_config_id: 'cfg-a', source: 'catalog' },
                { model_id: 'gpt-4o', provider_config_id: 'cfg-b', source: 'catalog' },
            ],
        };
        const models = processModelsResponse(resp);
        assert.equal(models[0].id, 'cfg-a:gpt-4o');
        assert.equal(models[1].id, 'cfg-b:gpt-4o');
    });
});

// ── Hidden models ────────────────────────────

describe('processModelsResponse — hidden models', () => {
    const apiResponse = {
        models: [
            { model_id: 'gpt-4o', provider_config_id: 'c1', config_id: 'c1', display_name: 'GPT-4o' },
            { model_id: 'claude-3', provider_config_id: 'c2', config_id: 'c2', display_name: 'Claude 3' },
        ],
    };

    it('marks models as hidden from user prefs (composite key)', () => {
        const hidden = new Set(['c1:gpt-4o']);
        const models = processModelsResponse(apiResponse, hidden);
        assert.equal(models[0].hidden, true, 'gpt-4o should be hidden');
        assert.equal(models[1].hidden, false, 'claude-3 should NOT be hidden');
    });

    it('personas are never hidden via model ID', () => {
        const resp = {
            models: [
                { model_id: 'gpt-4o', is_persona: true, persona_id: 'p1', display_name: 'Persona' },
            ],
        };
        const hidden = new Set([':gpt-4o']);
        const models = processModelsResponse(resp, hidden);
        assert.equal(models[0].hidden, false,
            'personas must NOT be hidden by base model hidden pref');
    });
});

// ── Empty / edge cases ───────────────────────

describe('processModelsResponse — edge cases', () => {
    it('handles empty models array', () => {
        const models = processModelsResponse({ models: [] });
        assert.equal(models.length, 0);
    });

    it('handles missing models key', () => {
        const models = processModelsResponse({});
        assert.equal(models.length, 0);
    });

    it('handles null response gracefully', () => {
        const models = processModelsResponse({ models: null });
        assert.equal(models.length, 0);
    });

    it('model without config_id uses bare model_id as ID', () => {
        const resp = {
            models: [{ model_id: 'test-model', display_name: 'Test' }],
        };
        const models = processModelsResponse(resp);
        assert.equal(models[0].id, 'test-model');
    });

    it('model without display_name or model_id uses id', () => {
        const resp = {
            models: [{ id: 'fallback-id' }],
        };
        const models = processModelsResponse(resp);
        assert.equal(models[0].id, 'fallback-id');
        assert.equal(models[0].name, 'fallback-id');
    });
});

// ── Model sorting ────────────────────────────

describe('Model sorting', () => {
    function sortModels(models) {
        const scopeOrder = { global: 0, team: 1, personal: 2 };
        return [...models].sort((a, b) => {
            if (a.isPersona && !b.isPersona) return -1;
            if (!a.isPersona && b.isPersona) return 1;
            if (a.isPersona && b.isPersona) {
                const sa = scopeOrder[a.personaScope] ?? 9;
                const sb = scopeOrder[b.personaScope] ?? 9;
                if (sa !== sb) return sa - sb;
            }
            return a.name.localeCompare(b.name);
        });
    }

    it('personas sort before regular models', () => {
        const models = [
            { name: 'GPT-4o', isPersona: false },
            { name: 'Code Helper', isPersona: true, personaScope: 'global' },
        ];
        const sorted = sortModels(models);
        assert.equal(sorted[0].name, 'Code Helper');
        assert.equal(sorted[1].name, 'GPT-4o');
    });

    it('global personas sort before team personas', () => {
        const models = [
            { name: 'Team Bot', isPersona: true, personaScope: 'team' },
            { name: 'Global Bot', isPersona: true, personaScope: 'global' },
            { name: 'My Bot', isPersona: true, personaScope: 'personal' },
        ];
        const sorted = sortModels(models);
        assert.equal(sorted[0].personaScope, 'global');
        assert.equal(sorted[1].personaScope, 'team');
        assert.equal(sorted[2].personaScope, 'personal');
    });

    it('regular models sort alphabetically', () => {
        const models = [
            { name: 'Zephyr', isPersona: false },
            { name: 'Claude', isPersona: false },
            { name: 'GPT-4o', isPersona: false },
        ];
        const sorted = sortModels(models);
        assert.equal(sorted[0].name, 'Claude');
        assert.equal(sorted[1].name, 'GPT-4o');
        assert.equal(sorted[2].name, 'Zephyr');
    });
});
