import { describe, it, expect } from 'bun:test';
import { MycoConfigSchema } from '@myco/config/schema';
import type { MycoConfig } from '@myco/config/schema';
import { withValue, withEmbedding, withTaskConfig } from '@myco/config/updates';

function baseConfig(): MycoConfig {
  return MycoConfigSchema.parse({ version: 3 });
}

describe('withValue', () => {
  it('sets a top-level field', () => {
    const result = withValue(baseConfig(), 'daemon.log_level', 'debug');
    expect(result.daemon.log_level).toBe('debug');
  });

  it('sets a nested field', () => {
    const result = withValue(baseConfig(), 'embedding.model', 'nomic-embed-text');
    expect(result.embedding.model).toBe('nomic-embed-text');
  });

  it('creates intermediate objects along the path', () => {
    const config = baseConfig();
    const result = withValue(config, 'agent.provider.type', 'ollama');
    expect(result.agent.provider?.type).toBe('ollama');
  });

  it('sets numeric values correctly', () => {
    const result = withValue(baseConfig(), 'capture.buffer_max_events', 1000);
    expect(result.capture.buffer_max_events).toBe(1000);
  });

  it('preserves all other fields unchanged', () => {
    const config = baseConfig();
    const result = withValue(config, 'embedding.model', 'nomic-embed-text');
    expect(result.embedding.provider).toBe('ollama');
    expect(result.daemon.log_level).toBe('info');
  });

  it('does not mutate the input config', () => {
    const config = baseConfig();
    const originalModel = config.embedding.model;
    withValue(config, 'embedding.model', 'nomic-embed-text');
    expect(config.embedding.model).toBe(originalModel);
  });
});

describe('withEmbedding', () => {
  it('merges partial embedding updates', () => {
    const result = withEmbedding(baseConfig(), { model: 'nomic-embed-text' });
    expect(result.embedding.model).toBe('nomic-embed-text');
    expect(result.embedding.provider).toBe('ollama');
  });

  it('replaces provider and model together', () => {
    const result = withEmbedding(baseConfig(), {
      provider: 'openrouter',
      model: 'text-embedding-3-small',
      base_url: 'https://openrouter.ai/api/v1',
    });
    expect(result.embedding.provider).toBe('openrouter');
    expect(result.embedding.model).toBe('text-embedding-3-small');
    expect(result.embedding.base_url).toBe('https://openrouter.ai/api/v1');
  });

  it('does not mutate the input config', () => {
    const config = baseConfig();
    withEmbedding(config, { model: 'nomic-embed-text' });
    expect(config.embedding.model).toBe('bge-m3');
  });

  it('preserves non-embedding sections', () => {
    const config = baseConfig();
    config.daemon.log_level = 'debug';
    const result = withEmbedding(config, { model: 'nomic-embed-text' });
    expect(result.daemon.log_level).toBe('debug');
  });
});

describe('withTaskConfig', () => {
  it('sets provider on a new task entry', () => {
    const result = withTaskConfig(baseConfig(), 'title-summary', {
      provider: { type: 'ollama', model: 'granite4:small-h' },
    });
    expect(result.agent.tasks?.['title-summary']?.provider?.type).toBe('ollama');
    expect(result.agent.tasks?.['title-summary']?.provider?.model).toBe('granite4:small-h');
  });

  it('preserves openai-compatible local_backend on task provider overrides', () => {
    const result = withTaskConfig(baseConfig(), 'title-summary', {
      provider: {
        type: 'openai-compatible',
        local_backend: 'lmstudio',
        base_url: 'http://localhost:1234',
        model: 'google/gemma-4-26b-a4b',
      },
    });
    expect(result.agent.tasks?.['title-summary']?.provider?.type).toBe('openai-compatible');
    expect(result.agent.tasks?.['title-summary']?.provider?.local_backend).toBe('lmstudio');
    expect(result.agent.tasks?.['title-summary']?.provider?.base_url).toBe('http://localhost:1234');
  });

  it('sets model and maxTurns', () => {
    const result = withTaskConfig(baseConfig(), 'title-summary', {
      model: 'granite4:small-h',
      maxTurns: 5,
    });
    expect(result.agent.tasks?.['title-summary']?.model).toBe('granite4:small-h');
    expect(result.agent.tasks?.['title-summary']?.maxTurns).toBe(5);
  });

  it('sets and clears task-level reasoningLevel', () => {
    let config = withTaskConfig(baseConfig(), 'title-summary', {
      reasoningLevel: 'high',
    });
    expect(config.agent.tasks?.['title-summary']?.reasoningLevel).toBe('high');
    // null clears the tier without disturbing a sibling model pin
    config = withTaskConfig(config, 'title-summary', { model: 'pinned-sku', reasoningLevel: null });
    expect(config.agent.tasks?.['title-summary']?.reasoningLevel).toBeUndefined();
    expect(config.agent.tasks?.['title-summary']?.model).toBe('pinned-sku');
  });

  it('null removes a field from an existing task', () => {
    let config = withTaskConfig(baseConfig(), 'title-summary', {
      model: 'granite4:small-h',
      maxTurns: 5,
    });
    config = withTaskConfig(config, 'title-summary', { maxTurns: null });
    expect(config.agent.tasks?.['title-summary']?.model).toBe('granite4:small-h');
    expect(config.agent.tasks?.['title-summary']?.maxTurns).toBeUndefined();
  });

  it('null provider removes the provider', () => {
    let config = withTaskConfig(baseConfig(), 'title-summary', {
      provider: { type: 'ollama', model: 'granite4:small-h' },
    });
    config = withTaskConfig(config, 'title-summary', { provider: null });
    expect(config.agent.tasks?.['title-summary']?.provider).toBeUndefined();
  });

  it('sets phase-level overrides', () => {
    const result = withTaskConfig(baseConfig(), 'vault-evolve', {
      phases: {
        extraction: {
          provider: { type: 'ollama', model: 'granite4:small-h' },
          maxTurns: 3,
        },
      },
    });
    const phase = result.agent.tasks?.['vault-evolve']?.phases?.extraction;
    expect(phase?.provider?.type).toBe('ollama');
    expect(phase?.maxTurns).toBe(3);
  });

  it('persists phase-level reasoningLevel overrides', () => {
    // Regression: the phase-override merge copied provider/model/maxTurns but
    // dropped reasoningLevel, so a UI tier change persisted as `extract: {}`
    // and the phase silently fell back to its YAML tier.
    const result = withTaskConfig(baseConfig(), 'vault-evolve', {
      phases: { extract: { reasoningLevel: 'default' } },
    });
    expect(result.agent.tasks?.['vault-evolve']?.phases?.extract?.reasoningLevel).toBe('default');
  });

  it('updates reasoningLevel while preserving other phase fields', () => {
    let config = withTaskConfig(baseConfig(), 'vault-evolve', {
      phases: { extract: { reasoningLevel: 'low', maxTurns: 35 } },
    });
    config = withTaskConfig(config, 'vault-evolve', {
      phases: { extract: { reasoningLevel: 'high' } },
    });
    const phase = config.agent.tasks?.['vault-evolve']?.phases?.extract;
    expect(phase?.reasoningLevel).toBe('high');
    expect(phase?.maxTurns).toBe(35);
  });

  it('null phase removes a specific phase', () => {
    let config = withTaskConfig(baseConfig(), 'vault-evolve', {
      phases: {
        extraction: { provider: { type: 'ollama' } },
        linking: { provider: { type: 'anthropic' } },
      },
    });
    config = withTaskConfig(config, 'vault-evolve', {
      phases: { extraction: null },
    });
    expect(config.agent.tasks?.['vault-evolve']?.phases?.extraction).toBeUndefined();
    expect(config.agent.tasks?.['vault-evolve']?.phases?.linking).toBeDefined();
  });

  it('removes empty task entries', () => {
    let config = withTaskConfig(baseConfig(), 'title-summary', {
      model: 'granite4:small-h',
    });
    config = withTaskConfig(config, 'title-summary', { model: null });
    expect(config.agent.tasks?.['title-summary']).toBeUndefined();
  });

  it('removes empty phases map', () => {
    let config = withTaskConfig(baseConfig(), 'test-task', {
      model: 'some-model',
      phases: { extraction: { maxTurns: 3 } },
    });
    config = withTaskConfig(config, 'test-task', {
      phases: { extraction: null },
    });
    expect(config.agent.tasks?.['test-task']?.phases).toBeUndefined();
    expect(config.agent.tasks?.['test-task']?.model).toBe('some-model');
  });

  it('does not mutate the input config', () => {
    const config = baseConfig();
    withTaskConfig(config, 'title-summary', {
      provider: { type: 'ollama' },
    });
    expect(config.agent.tasks).toBeUndefined();
  });

  it('preserves existing tasks when updating a different task', () => {
    let config = withTaskConfig(baseConfig(), 'task-a', { model: 'model-a' });
    config = withTaskConfig(config, 'task-b', { model: 'model-b' });
    expect(config.agent.tasks?.['task-a']?.model).toBe('model-a');
    expect(config.agent.tasks?.['task-b']?.model).toBe('model-b');
  });

  it('withTaskConfig persists schedule override', () => {
    const base = MycoConfigSchema.parse({ version: 3 });
    const updated = withTaskConfig(base, 'skill-survey', {
      schedule: { enabled: true, intervalSeconds: 600 },
    });
    expect(updated.agent.tasks?.['skill-survey']?.schedule?.enabled).toBe(true);
  });

  it('withTaskConfig persists schedule accelerator override', () => {
    const updated = withTaskConfig(baseConfig(), 'canopy-describe', {
      schedule: {
        accelerator: {
          name: 'canopy-pending-describe',
          thresholds: { steady: 25, accelerated: 250 },
        },
      },
    });
    expect(updated.agent.tasks?.['canopy-describe']?.schedule?.accelerator).toEqual({
      name: 'canopy-pending-describe',
      thresholds: { steady: 25, accelerated: 250 },
    });
  });

  it('withTaskConfig clears schedule accelerator with null', () => {
    let config = withTaskConfig(baseConfig(), 'canopy-describe', {
      schedule: {
        enabled: true,
        accelerator: {
          name: 'canopy-pending-describe',
          thresholds: { steady: 25, accelerated: 250 },
        },
      },
    });
    config = withTaskConfig(config, 'canopy-describe', {
      schedule: { accelerator: null },
    });
    expect(config.agent.tasks?.['canopy-describe']?.schedule?.enabled).toBe(true);
    expect(config.agent.tasks?.['canopy-describe']?.schedule?.accelerator).toBeUndefined();
  });

  it('withTaskConfig clears schedule with null', () => {
    const base = MycoConfigSchema.parse({
      version: 3,
      agent: { tasks: { 'skill-survey': { schedule: { enabled: true, intervalSeconds: 600 } } } },
    });
    const updated = withTaskConfig(base, 'skill-survey', { schedule: null });
    expect(updated.agent.tasks?.['skill-survey']?.schedule).toBeUndefined();
  });
});

// `withContext` was removed in config_version 8 along with the root
// `context:` block. All Cortex settings now live under `cortex.*` and
// flow through the scoped-settings patch endpoint or `updateConfig`
// directly — no bespoke helper or accompanying test block.

describe('unknown-key rejection (regression for canopy-describe params save)', () => {
  it('withTaskConfig persists params and round-trips them on disk shape', () => {
    const config = withTaskConfig(baseConfig(), 'canopy-describe', {
      params: { batch_size: 20 },
    });
    expect(config.agent.tasks?.['canopy-describe']?.params).toEqual({ batch_size: 20 });
  });

  it('withTaskConfig merges params over an existing override', () => {
    const a = withTaskConfig(baseConfig(), 'canopy-describe', {
      params: { batch_size: 20, max_description_chars: 200 },
    });
    const b = withTaskConfig(a, 'canopy-describe', { params: { batch_size: 30 } });
    expect(b.agent.tasks?.['canopy-describe']?.params).toEqual({
      batch_size: 30,
      max_description_chars: 200,
    });
  });

  it('withTaskConfig deletes params when set to null', () => {
    const a = withTaskConfig(baseConfig(), 'canopy-describe', {
      params: { batch_size: 20 },
    });
    const b = withTaskConfig(a, 'canopy-describe', { params: null });
    expect(b.agent.tasks?.['canopy-describe']?.params).toBeUndefined();
  });

  it('withTaskConfig throws on an unknown top-level key instead of silently dropping', () => {
    expect(() =>
      // @ts-expect-error — testing the runtime guard against a stale/wrong payload
      withTaskConfig(baseConfig(), 'canopy-describe', { batchSize: 20 }),
    ).toThrow(/unknown field 'batchSize'/);
  });

  it('withEmbedding throws on an unknown key', () => {
    expect(() =>
      // @ts-expect-error — runtime guard test
      withEmbedding(baseConfig(), { providerz: 'ollama' }),
    ).toThrow(/unknown field 'providerz'/);
  });

});
