// frontend/src/lib/models/__tests__/TranslationJobModel.test.ts
// #region TranslationJobModelFixTests [C:3] [TYPE Module] [SEMANTICS test,model,translate,field-mapping]
// @BRIEF L1 unit tests for TranslationJobModel field mapping invariants — no DOM render.
// @RELATION BINDS_TO -> [TranslationJobModel]
//
// @TEST_INVARIANT: disableReasoning-loaded-from-api -> VERIFIED_BY: [loads disableReasoning from API response, defaults to false when API omits field]
// @TEST_INVARIANT: disableReasoning-sent-in-save -> VERIFIED_BY: [sends disable_reasoning in save payload]
// @TEST_INVARIANT: databaseDialect-sent-in-save -> VERIFIED_BY: [sends database_dialect in save payload]
// @TEST_INVARIANT: datasourceSearch-populated-on-load -> VERIFIED_BY: [populates datasourceSearch on job load]
// #endregion
import { describe, it, expect, vi, beforeEach } from 'vitest';

// ── Module mocks ────────────────────────────────────────────────
vi.mock('$lib/api.js', () => ({
  api: {
    requestApi: vi.fn(),
    fetchApi: vi.fn(),
    postApi: vi.fn(),
    getEnvironmentDatabases: vi.fn(),
  },
}));

vi.mock('$lib/toasts.svelte.js', () => ({
  addToast: vi.fn(),
}));

vi.mock('$lib/i18n/index.svelte.js', () => ({
  _: (key: string) => key,
  getT: () => ({ translate: { config: { saved: 'Job saved', new_title: 'New Job', edit_title: 'Edit Job', run_started: 'Run started', run_failed: 'Run failed', insert_retry_started: 'Retry started', insert_retry_failed: 'Retry failed' }, preview: { title: 'Preview' }, run: { run_id: 'Run' }, schedule: { tab_label: 'Schedule' }, target_table_tab: 'Target Config' } }),
}));

vi.mock('$lib/stores/translationRun.svelte.js', () => ({
  startTranslationRun: vi.fn(),
  resetTranslationRun: vi.fn(),
  translationRunStore: { subscribe: vi.fn(), value: null },
}));

vi.mock('$lib/cot-logger', () => ({
  log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));

// ── Imports (after mocks) ───────────────────────────────────────
import { TranslationJobModel } from '../TranslationJobModel.svelte.ts';
import { api } from '$lib/api.js';
import { addToast } from '$lib/toasts.svelte.js';
import { startTranslationRun, resetTranslationRun } from '$lib/stores/translationRun.svelte.js';

// ── Helpers ─────────────────────────────────────────────────────
const JOB_ID = 'job-uuid-1111';
const ENV_ID = 'env-uuid-2222';
const DS_ID = 'ds-uuid-3333';

function makeJob(overrides: Record<string, unknown> = {}): Record<string, unknown> {
  return {
    id: JOB_ID, name: 'Test Job', description: 'A test translation job',
    source_datasource_id: DS_ID, source_dialect: 'postgresql', target_dialect: 'clickhouse',
    source_table: 'source_table', target_schema: 'target_schema', target_table: 'target_table',
    source_key_cols: ['id'], target_key_cols: ['id'], translation_column: 'text',
    target_column: 'translated_text', target_language_column: 'lang',
    target_source_column: 'source', target_source_language_column: 'source_lang',
    context_columns: ['category'], target_languages: ['en', 'de'],
    provider_id: 'provider-1', batch_size: 50, upsert_strategy: 'MERGE',
    status: 'DRAFT', database_dialect: 'postgresql', disable_reasoning: false,
    dictionary_ids: [], environment_id: ENV_ID, target_database_id: 'target-db-1',
    created_by: 'testuser', created_at: '2026-01-01T00:00:00Z', updated_at: null,
    ...overrides,
  };
}

function mockSuccessfulLoad(jobOverrides: Record<string, unknown> = {}, dsList?: Record<string, unknown>[]) {
  const job = makeJob(jobOverrides);
  const defaultDsList = [{ id: DS_ID, table_name: 'source_table', database_name: 'mydb', database_dialect: 'postgresql' }];
  const datasourcesResp = dsList ?? defaultDsList;
  vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
    if (url === '/llm/providers') return { providers: [] };
    if (url === '/translate/dictionaries?page_size=100') return { items: [] };
    if (url.startsWith('/translate/jobs/')) return job;
    return { items: [] };
  });
  vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);
  vi.mocked(api.fetchApi).mockImplementation(async (url: string) => {
    if (url.includes('/columns')) return { columns: [], virtual: [] };
    if (url.includes('/datasources?')) return datasourcesResp;
    if (url.includes('/runs')) return { items: [] };
    return undefined;
  });
  return job;
}

// ── Suite ───────────────────────────────────────────────────────
describe('TranslationJobModel — field mapping invariants', () => {
  let model: TranslationJobModel;

  beforeEach(() => {
    vi.clearAllMocks();
    model = new TranslationJobModel();
    model.jobId = JOB_ID;
    model.isNewJob = false;
    model.environments = [{ id: ENV_ID, name: 'TestEnv' }];
  });

  // ═══════════════════════════════════════════════════════════════
  // disableReasoning — load from API
  // ═══════════════════════════════════════════════════════════════
  describe('disableReasoning — load from API', () => {
    it('loads true from API response', async () => {
      mockSuccessfulLoad({ disable_reasoning: true });
      await model.loadInitialData();
      expect(model.disableReasoning).toBe(true);
    });
    it('loads false from API response', async () => {
      mockSuccessfulLoad({ disable_reasoning: false });
      await model.loadInitialData();
      expect(model.disableReasoning).toBe(false);
    });
    it('defaults to false when API omits disable_reasoning', async () => {
      const job = makeJob(); delete (job as Record<string, unknown>).disable_reasoning;
      mockSuccessfulLoad(job);
      await model.loadInitialData();
      expect(model.disableReasoning).toBe(false);
    });
    it('defaults to false when disable_reasoning is null', async () => {
      mockSuccessfulLoad({ disable_reasoning: null });
      await model.loadInitialData();
      expect(model.disableReasoning).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // disableReasoning — save to API
  // ═══════════════════════════════════════════════════════════════
  describe('disableReasoning — save to API', () => {
    it('sends disable_reasoning: true in save payload (POST)', async () => {
      model.disableReasoning = true; model.isNewJob = true;
      vi.mocked(api.requestApi).mockResolvedValue({ id: JOB_ID });
      await model.saveJob();
      expect(api.requestApi).toHaveBeenCalledWith('/translate/jobs', 'POST', expect.objectContaining({ disable_reasoning: true }));
    });
    it('sends disable_reasoning: false in save payload', async () => {
      model.disableReasoning = false; model.isNewJob = true;
      vi.mocked(api.requestApi).mockResolvedValue({ id: JOB_ID });
      await model.saveJob();
      const payload = vi.mocked(api.requestApi).mock.calls[0][2] as Record<string, unknown>;
      expect(payload.disable_reasoning).toBe(false);
    });
    it('sends disable_reasoning on existing job update (PUT)', async () => {
      model.disableReasoning = true; model.isNewJob = false; model.jobId = JOB_ID;
      vi.mocked(api.requestApi).mockResolvedValue({});
      await model.saveJob();
      expect(api.requestApi).toHaveBeenCalledWith(`/translate/jobs/${JOB_ID}`, 'PUT', expect.objectContaining({ disable_reasoning: true }));
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // databaseDialect — save to API
  // ═══════════════════════════════════════════════════════════════
  describe('databaseDialect — save to API', () => {
    it('sends database_dialect in save payload', async () => {
      model.databaseDialect = 'postgresql'; model.isNewJob = true;
      vi.mocked(api.requestApi).mockResolvedValue({ id: JOB_ID });
      await model.saveJob();
      const payload = vi.mocked(api.requestApi).mock.calls[0][2] as Record<string, unknown>;
      expect(payload.database_dialect).toBe('postgresql');
    });
    it('sends database_dialect as undefined when empty', async () => {
      model.databaseDialect = ''; model.isNewJob = true;
      vi.mocked(api.requestApi).mockResolvedValue({ id: JOB_ID });
      await model.saveJob();
      const payload = vi.mocked(api.requestApi).mock.calls[0][2] as Record<string, unknown>;
      expect(payload.database_dialect).toBeUndefined();
    });
    it('sends database_dialect on existing job update (PUT)', async () => {
      model.databaseDialect = 'mysql'; model.isNewJob = false; model.jobId = JOB_ID;
      vi.mocked(api.requestApi).mockResolvedValue({});
      await model.saveJob();
      expect(api.requestApi).toHaveBeenCalledWith(`/translate/jobs/${JOB_ID}`, 'PUT', expect.objectContaining({ database_dialect: 'mysql' }));
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // datasourceSearch — name lookup on load
  // ═══════════════════════════════════════════════════════════════
  describe('datasourceSearch — name lookup on load', () => {
    it('populates datasourceSearch when matching datasource found', async () => {
      mockSuccessfulLoad({}, [{ id: DS_ID, table_name: 'source_table', database_name: 'mydb', database_dialect: 'postgresql' }]);
      await model.loadInitialData();
      expect(model.datasourceSearch).toBe('source_table (mydb · postgresql)');
    });
    it('leaves datasourceSearch empty when datasource not found', async () => {
      mockSuccessfulLoad({}, []);
      await model.loadInitialData();
      expect(model.datasourceSearch).toBe('');
    });
    it('leaves datasourceSearch empty when no datasourceId', async () => {
      mockSuccessfulLoad({ source_datasource_id: '' });
      await model.loadInitialData();
      expect(model.datasourceSearch).toBe('');
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // uxState transitions during load
  // ═══════════════════════════════════════════════════════════════
  describe('uxState transition during loadInitialData', () => {
    it('sets uxState to loading then configured on success', async () => {
      mockSuccessfulLoad({});
      const promise = model.loadInitialData();
      expect(model.uxState).toBe('loading');
      await promise;
      expect(model.uxState).toBe('configured');
    });
    it('sets uxState to idle and stores error when job fetch fails', async () => {
      let callCount = 0;
      vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
        callCount++;
        if (callCount <= 2) return { items: [] };
        throw new Error('Job not found');
      });
      await model.loadInitialData();
      expect(model.uxState).toBe('idle');
      expect(model.error).toBe('Job not found');
    });
  });
});

// ═══════════════════════════════════════════════════════════════════
// Derived state & tabs
// ═══════════════════════════════════════════════════════════════════
describe('TranslationJobModel — Derived State', () => {
  let model: TranslationJobModel;
  beforeEach(() => { vi.clearAllMocks(); model = new TranslationJobModel(); });

  it('configValid requires translationColumn, datasourceId, targetLanguages, providerId', () => {
    expect(model.configValid).toBe(false);
    model.translationColumn = 'text'; model.datasourceId = DS_ID;
    model.targetLanguages = ['en']; model.providerId = 'p1';
    expect(model.configValid).toBe(true);
  });

  it('pageTitle shows new title for new job', () => {
    model.isNewJob = true;
    expect(model.pageTitle).toContain('New Job');
  });

  it('pageTitle includes job name for existing job', () => {
    model.isNewJob = false;
    model.existingJob = { name: 'My Job' } as any;
    expect(model.pageTitle).toContain('My Job');
  });

  it('tabs for new job exclude target and schedule', () => {
    model.isNewJob = true;
    const ids = model.tabs.map(t => t.id);
    expect(ids).toEqual(['config', 'preview', 'run']);
  });

  it('tabs for existing job include target and schedule', () => {
    model.isNewJob = false;
    model.existingJob = { id: 'j1' } as any;
    const ids = model.tabs.map(t => t.id);
    expect(ids).toContain('target');
    expect(ids).toContain('schedule');
  });
});

// ═══════════════════════════════════════════════════════════════════
// Run actions
// ═══════════════════════════════════════════════════════════════════
describe('TranslationJobModel — Run Actions', () => {
  let model: TranslationJobModel;
  beforeEach(() => { vi.clearAllMocks(); model = new TranslationJobModel(); model.jobId = JOB_ID; });

  it('handleTriggerRun calls postApi and stays isRunning (cleared by _onRunComplete)', async () => {
    vi.mocked(api.postApi).mockResolvedValue({ id: 'run-1' });
    await model.handleTriggerRun(false);
    expect(api.postApi).toHaveBeenCalledWith(
      `/translate/jobs/${JOB_ID}/run`,
      {},
    );
    // isRunning stays true after trigger — cleared by _onRunComplete callback
    expect(model.isRunning).toBe(true);
    expect(model.runError).toBe('');
  });

  it('handleTriggerRun handles errors', async () => {
    vi.mocked(api.postApi).mockRejectedValue(new Error('Trigger failed'));
    await model.handleTriggerRun(false);
    expect(model.runError).toBeTruthy();
    expect(model.isRunning).toBe(false);
  });

  it('loadRunHistory fetches and filters runs', async () => {
    vi.mocked(api.fetchApi).mockResolvedValue({ items: [{ id: JOB_ID }] });
    await model.loadRunHistory();
    expect(model.completedRuns).toHaveLength(1);
  });

  it('loadRunHistory resets on error', async () => {
    vi.mocked(api.fetchApi).mockRejectedValue(new Error('History failed'));
    await model.loadRunHistory();
    expect(model.completedRuns).toEqual([]);
  });

  it('toggleRunDetails adds and removes runIds', () => {
    expect(model.expandedRunIds).toEqual([]);
    model.toggleRunDetails('run-1');
    expect(model.expandedRunIds).toContain('run-1');
    model.toggleRunDetails('run-1');
    expect(model.expandedRunIds).not.toContain('run-1');
  });

  it('toggleRunDetails does nothing with empty id', () => {
    model.toggleRunDetails('');
    expect(model.expandedRunIds).toEqual([]);
  });

  it('handleRetryRun cancels current and re-triggers', async () => {
    vi.mocked(api.postApi).mockResolvedValue({ id: 'run-2' });
    const { translationRunStore } = await import('$lib/stores/translationRun.svelte.js');
    (translationRunStore as any).value = { runId: 'current-run' };
    await model.handleRetryRun();
    expect(resetTranslationRun).toHaveBeenCalled();
  });

  it('handleRetryInsert sends request when currentRunId exists', async () => {
    vi.mocked(api.requestApi).mockResolvedValue({});
    await model.handleRetryInsert();
    expect(api.requestApi).not.toHaveBeenCalled();
  });

  it('handleRetryInsert calls API when currentRunId is set', async () => {
    const { translationRunStore } = await import('$lib/stores/translationRun.svelte.js');
    (translationRunStore as any).value = { runId: JOB_ID };
    vi.mocked(api.postApi).mockResolvedValue({});
    model.jobId = JOB_ID;
    // currentRunId is now set from store
    await model.handleRetryInsert();
    expect(api.postApi).toHaveBeenCalledWith(`/translate/runs/${JOB_ID}/retry-insert`, {});
  });

  it('handleRetryInsert handles error', async () => {
    const { translationRunStore } = await import('$lib/stores/translationRun.svelte.js');
    (translationRunStore as any).value = { runId: JOB_ID };
    vi.mocked(api.postApi).mockRejectedValue(new Error('Retry failed'));
    await model.handleRetryInsert();
    // err.message takes precedence over i18n fallback
    expect(addToast).toHaveBeenCalledWith('Retry failed', 'error');
  });
});

// ═══════════════════════════════════════════════════════════════════
// Data loading helpers
// ═══════════════════════════════════════════════════════════════════
describe('TranslationJobModel — Data Loading Helpers', () => {
  let model: TranslationJobModel;
  beforeEach(() => { vi.clearAllMocks(); model = new TranslationJobModel(); });

  it('loadDatabases fetches when envId set', async () => {
    model.environmentId = ENV_ID;
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([{ id: 'db-1' }]);
    await model.loadDatabases();
    expect(model.databases).toHaveLength(1);
    expect(model.databasesLoading).toBe(false);
  });

  it('loadDatabases returns early without envId', async () => {
    await model.loadDatabases();
    expect(api.getEnvironmentDatabases).not.toHaveBeenCalled();
  });

  it('loadDatabases handles error', async () => {
    model.environmentId = ENV_ID;
    vi.mocked(api.getEnvironmentDatabases).mockRejectedValue(new Error('DB error'));
    await model.loadDatabases();
    expect(model.databases).toEqual([]);
    expect(model.databasesLoading).toBe(false);
  });

  it('loadDatasourceColumns fetches when datasourceId set', async () => {
    model.datasourceId = DS_ID; model.environmentId = ENV_ID;
    vi.mocked(api.fetchApi).mockResolvedValue({ columns: [{ name: 'col1' }], virtual: [{ name: 'vcol1' }] });
    await model.loadDatasourceColumns();
    expect(model.availableColumns).toHaveLength(1);
    expect(model.virtualColumns).toHaveLength(1);
  });

  it('loadDatasourceColumns returns early without datasourceId', async () => {
    await model.loadDatasourceColumns();
    expect(api.fetchApi).not.toHaveBeenCalled();
  });

  it('loadDatasourceColumns handles error', async () => {
    model.datasourceId = DS_ID; model.environmentId = ENV_ID;
    vi.mocked(api.fetchApi).mockRejectedValue(new Error('Cols failed'));
    await model.loadDatasourceColumns();
    expect(model.availableColumns).toEqual([]);
  });

  it('handleEnvChange resets targetDatabaseId and loads databases', async () => {
    model.targetDatabaseId = 'old-db';
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);
    model.handleEnvChange(ENV_ID);
    expect(model.environmentId).toBe(ENV_ID);
    expect(model.targetDatabaseId).toBe('');
    expect(api.getEnvironmentDatabases).toHaveBeenCalled();
  });

  it('loadRunHistory filters runs with invalid ids', async () => {
    vi.mocked(api.fetchApi).mockResolvedValue({ items: [{ id: '' }, { id: '  ' }, { id: 'valid-id' }] });
    await model.loadRunHistory();
    expect(model.completedRuns).toHaveLength(1);
  });

  it('_onRunComplete sets state and loads history', async () => {
    const { translationRunStore } = await import('$lib/stores/translationRun.svelte.js');
    model.jobId = JOB_ID;
    vi.mocked(api.fetchApi).mockResolvedValue({ items: [] });
    vi.mocked(api.postApi).mockResolvedValue({});
    (translationRunStore as any).value = { runId: 'run-1' };
    model.isRunning = true;
    model['_onRunComplete']({ status: 'COMPLETED' });
    expect(model.isRunning).toBe(false);
    expect(model.runComplete).toBe(true);
  });

  it('_onRunComplete skips reload when CANCELLED', () => {
    model.jobId = JOB_ID;
    model['_onRunComplete']({ status: 'CANCELLED' });
    expect(model.runComplete).toBe(true);
  });

  it('_onRunComplete skips if already runComplete', () => {
    model.runComplete = true;
    model['_onRunComplete']({});
    // no crash
  });

  it('columnList returns availableColumns', () => {
    model.availableColumns = [{ name: 'col1' }] as any;
    expect(model.columnList).toHaveLength(1);
  });

  it('logicalColumns filters out non-physical columns', () => {
    model.availableColumns = [
      { name: 'col1', is_physical: true } as any,
      { name: 'col2', is_physical: false } as any,
      { name: 'col3' } as any,
    ];
    expect(model.logicalColumns).toHaveLength(2);
  });

  it('loadInitialData handles providers as array', async () => {
    model.isNewJob = false;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') return [{ id: 'p1', name: 'Provider 1' }];
      if (url === '/translate/dictionaries?page_size=100') return { items: [] };
      if (url.startsWith('/translate/jobs/')) return makeJob();
      return { items: [] };
    });
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);
    vi.mocked(api.fetchApi).mockImplementation(async () => ({ columns: [], virtual: [] }));
    await model.loadInitialData();
    expect(model.llmProviders).toHaveLength(1);
  });

  it('loadInitialData handles providers as results format', async () => {
    model.isNewJob = false;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') return { results: [{ id: 'p2', name: 'Provider 2' }] };
      if (url === '/translate/dictionaries?page_size=100') return { items: [] };
      if (url.startsWith('/translate/jobs/')) return makeJob();
      return { items: [] };
    });
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);
    vi.mocked(api.fetchApi).mockImplementation(async () => ({ columns: [], virtual: [] }));
    await model.loadInitialData();
    expect(model.llmProviders).toHaveLength(1);
  });

  it('loadInitialData handles error in providers fetch', async () => {
    model.isNewJob = false;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') { throw new Error('LLM down'); }
      if (url === '/translate/dictionaries?page_size=100') return { items: [] };
      if (url.startsWith('/translate/jobs/')) return makeJob();
      return { items: [] };
    });
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);
    vi.mocked(api.fetchApi).mockImplementation(async () => ({ columns: [], virtual: [] }));
    await model.loadInitialData();
    expect(model.llmProviders).toEqual([]);
  });

  it('loadInitialData populates datasourceSearch with best-effort lookup failure', async () => {
    model.isNewJob = false;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') return { providers: [] };
      if (url === '/translate/dictionaries?page_size=100') return { items: [] };
      if (url.startsWith('/translate/jobs/')) return makeJob({ source_datasource_id: 'nonexistent' });
      return { items: [] };
    });
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);
    vi.mocked(api.fetchApi).mockImplementation(async (url: string) => {
      if (url.includes('/datasources?')) return [];
      return { columns: [], virtual: [] };
    });
    await model.loadInitialData();
    expect(model.datasourceSearch).toBe('');
  });

  it('handleTriggerRun with full=true', async () => {
    vi.mocked(api.postApi).mockResolvedValue({ id: 'run-full' });
    await model.handleTriggerRun(true);
    expect(model.isFullRun).toBe(true);
  });

  it('handleRetryRun cancels current run', async () => {
    vi.mocked(api.postApi).mockResolvedValue({ id: 'run-3' });
    const { translationRunStore, resetTranslationRun } = await import('$lib/stores/translationRun.svelte.js');
    (translationRunStore as any).value = { runId: 'current-run' };
    vi.mocked(api.requestApi).mockResolvedValue({});
    await model.handleRetryRun();
    expect(resetTranslationRun).toHaveBeenCalled();
  });

  it('saveJob handles validation error', async () => {
    vi.mocked(api.requestApi).mockRejectedValue(new Error('Validation error'));
    await model.saveJob();
    expect(model.uxState).toBe('validation_error');
    expect(model.error).toBe('Validation error');
  });

  it('saveJob creates new job and sets jobId', async () => {
    model.isNewJob = true;
    vi.mocked(api.requestApi).mockResolvedValue({ id: 'new-job-id' });
    await model.saveJob();
    expect(model.jobId).toBe('new-job-id');
    expect(model.isNewJob).toBe(false);
    expect(model.uxState).toBe('configured');
  });

  it('toggleRunDetails does nothing with undefined', () => {
    model.toggleRunDetails('' as any);
    expect(model.expandedRunIds).toEqual([]);
  });

  it('virtualColumnNames returns virtualColumns', () => {
    model.virtualColumns = [{ name: 'vcol1' }] as any;
    expect(model.virtualColumnNames).toHaveLength(1);
  });

  it('loadRunHistory filters expandedRunIds', async () => {
    model.expandedRunIds = ['run-1', 'run-removed'];
    vi.mocked(api.fetchApi).mockResolvedValue({ items: [{ id: 'run-1' }] });
    await model.loadRunHistory();
    expect(model.expandedRunIds).toEqual(['run-1']);
  });

  it('loadInitialData catches datasource lookup error', async () => {
    model.isNewJob = false;
    let callCount = 0;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      callCount++;
      if (callCount <= 2) return { providers: [], items: [] };
      if (url.startsWith('/translate/jobs/')) return makeJob({ environment_id: 'env-1', source_datasource_id: 'ds-1' });
      return { items: [] };
    });
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);
    vi.mocked(api.fetchApi).mockImplementation(async (url: string) => {
      if (url.includes('/datasources?')) throw new Error('Lookup error');
      return { columns: [], virtual: [] };
    });
    await model.loadInitialData();
    expect(model.datasourceSearch).toBe('');
  });

  it('loadInitialData catch handler on dictionaries failure', async () => {
    model.isNewJob = true;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') return { providers: [] };
      if (url === '/translate/dictionaries?page_size=100') throw new Error('Dicts down');
      return { items: [] };
    });
    await model.loadInitialData();
    expect(model.uxState).toBe('configured');
  });

  it('loadInitialData handles providers without providers/results wrapper', async () => {
    model.isNewJob = true;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') return { not_providers: true };
      if (url === '/translate/dictionaries?page_size=100') return { items: [] };
      return { items: [] };
    });
    await model.loadInitialData();
    expect(model.llmProviders).toEqual([]);
    expect(model.uxState).toBe('configured');
  });

  it('loadInitialData handles providers with null providers field', async () => {
    model.isNewJob = true;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') return { providers: null };
      if (url === '/translate/dictionaries?page_size=100') return { items: [] };
      return { items: [] };
    });
    await model.loadInitialData();
    expect(model.llmProviders).toEqual([]);
  });

  it('loadInitialData handles dictionaries without items/results wrapper', async () => {
    model.isNewJob = true;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') return { providers: [] };
      if (url === '/translate/dictionaries?page_size=100') return { not_items: true };
      return { items: [] };
    });
    await model.loadInitialData();
    expect(model.availableDictionaries).toEqual([]);
    expect(model.uxState).toBe('configured');
  });

  it('loadInitialData datasourceSearch with non-array list from fetchDatasources', async () => {
    model.isNewJob = false;
    vi.mocked(api.requestApi).mockImplementation(async (url: string) => {
      if (url === '/llm/providers') return { providers: [] };
      if (url === '/translate/dictionaries?page_size=100') return { items: [] };
      if (url.startsWith('/translate/jobs/')) return makeJob({ environment_id: 'env-1', source_datasource_id: 'ds-1' });
      return { items: [] };
    });
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue([]);
    // fetchDatasources returns null (non-array) — triggers Array.isArray(list) false branch
    vi.mocked(api.fetchApi).mockImplementation(async (url: string) => {
      if (url.includes('/datasources?')) return null;  // non-array response
      return { columns: [], virtual: [] };
    });
    await model.loadInitialData();
    expect(model.datasourceSearch).toBe('');  // no match from empty list
  });

  it('saveJob does not set jobId or existingJob when resp has no id', async () => {
    model.isNewJob = true;
    model.jobId = '';
    vi.mocked(api.requestApi).mockResolvedValue({ status: 'ok' });  // no id field
    await model.saveJob();
    // resp.id is undefined, isNewJob is true, but resp?.id is falsy → skip block
    expect(model.isNewJob).toBe(true);  // still new job
    expect(model.jobId).toBe('');  // not set
    expect(model.uxState).toBe('configured');
  });

  it('saveJob handles API error with string rejection', async () => {
    vi.mocked(api.requestApi).mockRejectedValue('server error string');
    await model.saveJob();
    expect(model.uxState).toBe('validation_error');
    expect(model.error).toBe('Failed to save');
  });

  it('handleRetryRun without currentRunId does not call cancel', async () => {
    vi.mocked(api.postApi).mockResolvedValue({ id: 'run-1' });
    await model.handleRetryRun();
    // currentRunId is null (from store), so cancelRun is not called
    expect(api.postApi).toHaveBeenCalledWith(expect.stringContaining('/run'), {});
  });

  it('handleRetryInsert without currentRunId returns early', async () => {
    const { translationRunStore } = await import('$lib/stores/translationRun.svelte.js');
    (translationRunStore as any).value = null;  // ensure no current run
    await model.handleRetryInsert();
    // currentRunId is null → api.postApi not called
    expect(api.postApi).not.toHaveBeenCalled();
  });

  it('handleTriggerRun handles non-Error rejection', async () => {
    vi.mocked(api.postApi).mockRejectedValue('string error');
    await model.handleTriggerRun(false);
    expect(model.runError).toBe('translate.config.run_failed');
    expect(model.isRunning).toBe(false);
  });

  it('_onRunComplete with CANCELLED status skips loadRunHistory', async () => {
    model['_onRunComplete']({ status: 'CANCELLED' });
    expect(model.isRunning).toBe(false);
    expect(model.runComplete).toBe(true);
    // Should not have called fetchRunHistory / api.fetchApi
    expect(api.fetchApi).not.toHaveBeenCalled();
  });

  it('handleEnvChange with empty envId does not change env', () => {
    model.environmentId = 'current-env';
    model.handleEnvChange('');
    // handleEnvChange sets environmentId but loadDatabases returns early
    expect(model.environmentId).toBe('');
    expect(api.getEnvironmentDatabases).not.toHaveBeenCalled();
  });

  it('loadDatabases handles non-array response (Array.isArray false)', async () => {
    model.environmentId = ENV_ID;
    vi.mocked(api.getEnvironmentDatabases).mockResolvedValue({ not_an_array: true } as any);
    await model.loadDatabases();
    // Array.isArray(res) is false → uses [] as databases
    expect(model.databases).toEqual([]);
    expect(model.databasesLoading).toBe(false);
  });

  it('loadDatasourceColumns handles null columns/virtual response', async () => {
    model.datasourceId = DS_ID; model.environmentId = ENV_ID;
    vi.mocked(api.fetchApi).mockResolvedValue({ columns: null, virtual: null });
    await model.loadDatasourceColumns();
    // res?.columns || [] → null || [] → []
    // res?.virtual || [] → null || [] → []
    expect(model.availableColumns).toEqual([]);
    expect(model.virtualColumns).toEqual([]);
  });

  it('loadDatasourceColumns handles undefined columns/virtual response', async () => {
    model.datasourceId = DS_ID; model.environmentId = ENV_ID;
    vi.mocked(api.fetchApi).mockResolvedValue({});  // no columns/virtual fields at all
    await model.loadDatasourceColumns();
    expect(model.availableColumns).toEqual([]);
    expect(model.virtualColumns).toEqual([]);
  });

  it('isRunning starts false', () => {
    expect(model.isRunning).toBe(false);
  });

  it('currentRunId returns null when store value is null', () => {
    expect(model.currentRunId).toBeNull();
  });

  it('saveJob existing job update on api failure sets error', async () => {
    model.isNewJob = false;
    model.jobId = JOB_ID;
    model.existingJob = { id: JOB_ID };
    vi.mocked(api.requestApi).mockRejectedValue(new Error('Update failed'));
    await model.saveJob();
    expect(model.uxState).toBe('validation_error');
    expect(model.error).toBe('Update failed');
  });
});
