// ==========================================
// User Journey Model Tests — E2E Frontend
// ==========================================
// These tests feed the ACTUAL processModelsResponse()
// (same code as fetchModels() in app.js) with backend
// response data shaped EXACTLY like GET /models/enabled
// for every user permutation.
//
// If these pass but the UI is broken, the bug is in
// the DOM rendering — not the data pipeline.
//
// Run: node --test src/js/__tests__/user-journey-models.test.js
// ==========================================

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

// ── Backend response factories ──────────────────
// These match the EXACT JSON shape from GET /models/enabled
// (Go struct: models.UserModel serialized as JSON)

function globalModel(modelId, configId, providerName) {
    return {
        id: modelId,
        model_id: modelId,
        display_name: modelId,
        provider_config_id: configId,
        config_id: configId,
        provider_name: providerName,
        provider_type: 'openai',
        source: 'catalog',
        scope: 'global',
        is_persona: false,
        capabilities: { streaming: true },
        hidden: false,
    };
}

function teamModel(modelId, configId, providerName, teamName) {
    return {
        id: modelId,
        model_id: modelId,
        display_name: modelId,
        provider_config_id: configId,
        config_id: configId,
        provider_name: providerName,
        provider_type: 'venice',
        source: 'catalog',
        scope: 'team',
        is_persona: false,
        capabilities: { streaming: true },
        hidden: false,
        team_name: teamName,
    };
}

function personalModel(modelId, configId, providerName) {
    return {
        id: modelId,
        model_id: modelId,
        display_name: modelId,
        provider_config_id: configId,
        config_id: configId,
        provider_name: providerName,
        provider_type: 'venice',
        source: 'catalog',
        scope: 'personal',
        is_persona: false,
        capabilities: { streaming: true },
        hidden: false,
    };
}

function personaModel(personaId, baseModelId, scope, teamName) {
    return {
        id: personaId,
        model_id: baseModelId,
        display_name: `Persona: ${baseModelId}`,
        persona_id: personaId,
        persona_scope: scope,
        persona_team_name: teamName || '',
        is_persona: true,
        source: 'persona',
        scope: scope,
        capabilities: { streaming: true },
    };
}

// ── Shared fixtures ──────────────────────────────

const GLOBAL_CONFIG_ID = 'cfg-global-openai';
const TEAM_CONFIG_ID = 'cfg-team-venice';
const BYOK_CONFIG_ID = 'cfg-personal-venice';

const GLOBAL_MODELS = [
    globalModel('gpt-4o', GLOBAL_CONFIG_ID, 'OpenAI Production'),
    globalModel('gpt-4o-mini', GLOBAL_CONFIG_ID, 'OpenAI Production'),
];

const TEAM_MODELS = [
    teamModel('llama-3.3-70b', TEAM_CONFIG_ID, 'Team Venice', 'Engineering'),
];

const BYOK_MODELS = [
    personalModel('llama-3.3-70b', BYOK_CONFIG_ID, 'My Venice Key'),
    personalModel('deepseek-r1', BYOK_CONFIG_ID, 'My Venice Key'),
];

// ═══════════════════════════════════════════════
// USER PERMUTATION TESTS
// ═══════════════════════════════════════════════

describe('User Journey: Platform Admin (no team, no BYOK)', () => {
    // Backend returns: 2 global enabled models
    const apiResponse = { models: [...GLOBAL_MODELS] };
    const models = processModelsResponse(apiResponse);

    it('sees exactly 2 models', () => {
        assert.equal(models.length, 2);
    });

    it('all models have composite IDs (configId:modelId)', () => {
        assert.equal(models[0].id, `${GLOBAL_CONFIG_ID}:gpt-4o`);
        assert.equal(models[1].id, `${GLOBAL_CONFIG_ID}:gpt-4o-mini`);
    });

    it('all models have configId for routing', () => {
        for (const m of models) {
            assert.ok(m.configId, `model ${m.baseModelId} must have configId`);
            assert.equal(m.configId, GLOBAL_CONFIG_ID);
        }
    });

    it('all models have provider name for display', () => {
        for (const m of models) {
            assert.equal(m.provider, 'OpenAI Production');
        }
    });

    it('no models are hidden by default', () => {
        for (const m of models) {
            assert.equal(m.hidden, false);
        }
    });

    it('source is catalog (not persona)', () => {
        for (const m of models) {
            assert.equal(m.isPersona, false);
            assert.equal(m.source, 'catalog');
        }
    });
});

describe('User Journey: Team Member (global + team)', () => {
    // Backend returns: 2 global + 1 team model
    const apiResponse = { models: [...GLOBAL_MODELS, ...TEAM_MODELS] };
    const models = processModelsResponse(apiResponse);

    it('sees exactly 3 models', () => {
        assert.equal(models.length, 3);
    });

    it('team model has different configId from global', () => {
        const teamModel = models.find(m => m.baseModelId === 'llama-3.3-70b');
        assert.ok(teamModel, 'llama-3.3-70b should be present');
        assert.equal(teamModel.configId, TEAM_CONFIG_ID);
        assert.notEqual(teamModel.configId, GLOBAL_CONFIG_ID);
    });

    it('team model composite ID is unique from global with same model_id', () => {
        // If both global AND team have "llama-3.3-70b", they must have different composite IDs
        const globalLlama = globalModel('llama-3.3-70b', GLOBAL_CONFIG_ID, 'OpenAI');
        const resp = { models: [globalLlama, ...TEAM_MODELS] };
        const result = processModelsResponse(resp);

        const ids = result.map(m => m.id);
        const uniqueIds = new Set(ids);
        assert.equal(ids.length, uniqueIds.size,
            `IDs must be unique: ${JSON.stringify(ids)}`);
    });

    it('team model has correct source', () => {
        const teamModel = models.find(m => m.baseModelId === 'llama-3.3-70b');
        // source comes from backend — "catalog" for both global and team
        assert.equal(teamModel.source, 'catalog');
    });
});

describe('User Journey: BYOK User (global + personal)', () => {
    // Backend returns: 2 global + 2 personal models
    const apiResponse = { models: [...GLOBAL_MODELS, ...BYOK_MODELS] };
    const models = processModelsResponse(apiResponse);

    it('sees exactly 4 models', () => {
        assert.equal(models.length, 4);
    });

    it('personal models have unique composite IDs', () => {
        const personal = models.filter(m => m.configId === BYOK_CONFIG_ID);
        assert.equal(personal.length, 2, 'should have 2 personal models');
        assert.equal(personal[0].id, `${BYOK_CONFIG_ID}:llama-3.3-70b`);
        assert.equal(personal[1].id, `${BYOK_CONFIG_ID}:deepseek-r1`);
    });

    it('personal and global models with same model_id have different composite IDs', () => {
        // Both global and personal might have "llama-3.3-70b" — must not collide
        const globalLlama = globalModel('llama-3.3-70b', GLOBAL_CONFIG_ID, 'OpenAI');
        const personalLlama = personalModel('llama-3.3-70b', BYOK_CONFIG_ID, 'My Key');
        const resp = { models: [globalLlama, personalLlama] };
        const result = processModelsResponse(resp);

        assert.equal(result[0].id, `${GLOBAL_CONFIG_ID}:llama-3.3-70b`);
        assert.equal(result[1].id, `${BYOK_CONFIG_ID}:llama-3.3-70b`);
        assert.notEqual(result[0].id, result[1].id);
    });

    it('personal model provider name shows user key name, not global', () => {
        const personal = models.find(m => m.configId === BYOK_CONFIG_ID);
        assert.equal(personal.provider, 'My Venice Key');
    });
});

describe('User Journey: Team Member + BYOK (global + team + personal)', () => {
    // Backend returns: 2 global + 1 team + 2 personal
    const apiResponse = { models: [...GLOBAL_MODELS, ...TEAM_MODELS, ...BYOK_MODELS] };
    const models = processModelsResponse(apiResponse);

    it('sees exactly 5 models', () => {
        assert.equal(models.length, 5);
    });

    it('all three config IDs are represented', () => {
        const configIds = new Set(models.map(m => m.configId));
        assert.ok(configIds.has(GLOBAL_CONFIG_ID), 'missing global');
        assert.ok(configIds.has(TEAM_CONFIG_ID), 'missing team');
        assert.ok(configIds.has(BYOK_CONFIG_ID), 'missing personal');
    });

    it('no ID collisions across scopes', () => {
        const ids = models.map(m => m.id);
        const uniqueIds = new Set(ids);
        assert.equal(ids.length, uniqueIds.size,
            `composite IDs must be unique across scopes: ${JSON.stringify(ids)}`);
    });
});

describe('User Journey: Outsider (global only, no team, no BYOK)', () => {
    const apiResponse = { models: [...GLOBAL_MODELS] };
    const models = processModelsResponse(apiResponse);

    it('sees exactly 2 global models', () => {
        assert.equal(models.length, 2);
    });

    it('no team or personal models leak in', () => {
        for (const m of models) {
            assert.equal(m.configId, GLOBAL_CONFIG_ID);
        }
    });
});

// ═══════════════════════════════════════════════
// EDGE CASES FROM REAL BUGS
// ═══════════════════════════════════════════════

describe('Edge: Empty model list', () => {
    it('handles empty array', () => {
        const models = processModelsResponse({ models: [] });
        assert.equal(models.length, 0);
    });

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

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

describe('Edge: Missing provider_config_id', () => {
    it('falls back to model_id only (no composite ID)', () => {
        const resp = { models: [{
            model_id: 'gpt-4o',
            display_name: 'GPT-4o',
            // NO provider_config_id, NO config_id
            source: 'catalog',
            scope: 'global',
        }]};
        const models = processModelsResponse(resp);
        assert.equal(models[0].id, 'gpt-4o',
            'without configId, ID must fall back to bare model_id');
        assert.equal(models[0].configId, null);
    });
});

describe('Edge: provider_config_id only (no config_id alias)', () => {
    it('uses provider_config_id for composite ID', () => {
        const resp = { models: [{
            model_id: 'gpt-4o',
            provider_config_id: 'cfg-uuid',
            // NO config_id alias
            provider_name: 'OpenAI',
            source: 'catalog',
        }]};
        const models = processModelsResponse(resp);
        assert.equal(models[0].id, 'cfg-uuid:gpt-4o');
        assert.equal(models[0].configId, 'cfg-uuid');
    });
});

describe('Edge: Hidden model preferences', () => {
    it('marks hidden models correctly', () => {
        const hidden = new Set(['cfg-global-openai:gpt-4o-mini']);
        const models = processModelsResponse({ models: GLOBAL_MODELS }, hidden);
        const mini = models.find(m => m.baseModelId === 'gpt-4o-mini');
        const main = models.find(m => m.baseModelId === 'gpt-4o');
        assert.equal(mini.hidden, true, 'gpt-4o-mini should be hidden');
        assert.equal(main.hidden, false, 'gpt-4o should not be hidden');
    });

    it('personas are never hidden', () => {
        const persona = personaModel('preset-1', 'gpt-4o', 'global');
        const hidden = new Set([':gpt-4o']);
        const models = processModelsResponse({ models: [persona] }, hidden);
        assert.equal(models[0].hidden, false, 'personas must never be hidden');
    });
});

describe('Edge: Personas mixed with catalog models', () => {
    const apiResponse = {
        models: [
            ...GLOBAL_MODELS,
            personaModel('preset-code', 'gpt-4o', 'global'),
            personaModel('preset-team', 'llama-3.3-70b', 'team', 'Engineering'),
        ],
    };
    const models = processModelsResponse(apiResponse);

    it('personas use persona_id as ID, not composite', () => {
        const persona = models.find(m => m.personaId === 'preset-code');
        assert.ok(persona, 'preset-code should be present');
        assert.equal(persona.id, 'preset-code');
        assert.equal(persona.isPersona, true);
    });

    it('persona and catalog model with same model_id have different IDs', () => {
        const catalog = models.find(m => !m.isPersona && m.baseModelId === 'gpt-4o');
        const persona = models.find(m => m.isPersona && m.baseModelId === 'gpt-4o');
        assert.ok(catalog, 'catalog gpt-4o should exist');
        assert.ok(persona, 'preset gpt-4o should exist');
        assert.notEqual(catalog.id, persona.id);
    });

    it('persona team name flows through', () => {
        const teamPersona = models.find(m => m.personaId === 'preset-team');
        assert.equal(teamPersona.personaTeamName, 'Engineering');
    });
});

// ═══════════════════════════════════════════════
// FULL MATRIX: 4 actors × expected model counts
// ═══════════════════════════════════════════════
// This matches the backend TestUserJourney_FullMatrix
// and proves the frontend handles the same data correctly.

describe('Full Matrix: 4 actors × backend response → frontend model count', () => {
    const matrix = [
        {
            actor: 'platformAdmin',
            models: [...GLOBAL_MODELS],
            expectedCount: 2,
            desc: 'global only',
        },
        {
            actor: 'teamAdmin',
            models: [...GLOBAL_MODELS, ...TEAM_MODELS],
            expectedCount: 3,
            desc: 'global + team',
        },
        {
            actor: 'teamMember',
            models: [...GLOBAL_MODELS, ...TEAM_MODELS, ...BYOK_MODELS],
            expectedCount: 5,
            desc: 'global + team + personal',
        },
        {
            actor: 'outsider',
            models: [...GLOBAL_MODELS,
                personalModel('llama-outsider-byok', 'cfg-outsider', 'Outsider Key')],
            expectedCount: 3,
            desc: 'global + own BYOK',
        },
    ];

    for (const tc of matrix) {
        it(`${tc.actor} (${tc.desc}): ${tc.expectedCount} models`, () => {
            const result = processModelsResponse({ models: tc.models });
            assert.equal(result.length, tc.expectedCount,
                `${tc.actor} should see ${tc.expectedCount}, got ${result.length}: ` +
                result.map(m => `${m.baseModelId}(${m.configId})`).join(', '));
        });

        it(`${tc.actor}: all IDs unique`, () => {
            const result = processModelsResponse({ models: tc.models });
            const ids = result.map(m => m.id);
            const unique = new Set(ids);
            assert.equal(ids.length, unique.size,
                `${tc.actor}: duplicate IDs: ${JSON.stringify(ids)}`);
        });

        it(`${tc.actor}: all models have configId or are personas`, () => {
            const result = processModelsResponse({ models: tc.models });
            for (const m of result) {
                if (!m.isPersona) {
                    assert.ok(m.configId,
                        `${tc.actor}: model ${m.baseModelId} missing configId — ` +
                        `frontend will fail to route completions`);
                }
            }
        });
    }
});
