import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import YAML from 'yaml';
import { run } from '@myco/cli/config';
import { MycoConfigSchema } from '@myco/config/schema';
import { resolveServiceDaemonStatePath } from '@myco/grove/paths';
import { sandboxMycoHome } from '../helpers/myco-home-sandbox.js';

const VALID_CONFIG = {
  version: 3,
  config_version: 0,
  embedding: { provider: 'ollama', model: 'bge-m3' },
  daemon: { port: null, log_level: 'info' },
  capture: {
    transcript_paths: [],
    artifact_watch: ['.claude/plans/', '.cursor/plans/'],
    artifact_extensions: ['.md'],
    buffer_max_events: 500,
  },
};

function writeConfig(dir: string, config: Record<string, unknown> = VALID_CONFIG): void {
  fs.writeFileSync(path.join(dir, 'myco.yaml'), YAML.stringify(config), 'utf-8');
}

function readConfig(dir: string): Record<string, unknown> {
  return YAML.parse(fs.readFileSync(path.join(dir, 'myco.yaml'), 'utf-8')) as Record<string, unknown>;
}

describe('myco config', () => {
  let tmpDir: string;
  let originalLog: typeof console.log;
  let originalError: typeof console.error;
  let logged: string[];
  let errors: string[];
  let exitCode: number | undefined;
  let sandbox: ReturnType<typeof sandboxMycoHome>;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'myco-config-test-'));
    // The daemon-restart-notice tests write and unlink a daemon.json via
    // resolveServiceDaemonStatePath(); the sandbox keeps that away from
    // the machine's real ~/.myco.
    sandbox = sandboxMycoHome('myco-config-home-');
    logged = [];
    errors = [];
    exitCode = undefined;
    originalLog = console.log;
    originalError = console.error;
    console.log = (...args: unknown[]) => logged.push(args.join(' '));
    console.error = (...args: unknown[]) => errors.push(args.join(' '));
    // Mock process.exit to capture exit code instead of terminating
    const originalExit = process.exit;
    process.exit = ((code?: number) => {
      exitCode = code;
      throw new Error(`process.exit(${code})`);
    }) as never;
    // Store for cleanup
    (globalThis as Record<string, unknown>).__originalExit = originalExit;
  });

  afterEach(() => {
    console.log = originalLog;
    console.error = originalError;
    process.exit = (globalThis as Record<string, unknown>).__originalExit as typeof process.exit;
    fs.rmSync(tmpDir, { recursive: true, force: true });
    sandbox.restore();
  });

  describe('config get', () => {
    it('gets a nested value via dot-path', async () => {
      writeConfig(tmpDir);
      await run(['get', 'embedding.model'], tmpDir);
      expect(logged).toContain('bge-m3');
    });

    it('gets a top-level value', async () => {
      writeConfig(tmpDir);
      await run(['get', 'version'], tmpDir);
      expect(logged).toContain('3');
    });

    it('prints object values as JSON', async () => {
      writeConfig(tmpDir);
      await run(['get', 'embedding'], tmpDir);
      const parsed = JSON.parse(logged[0]);
      expect(parsed.provider).toBe('ollama');
      expect(parsed.model).toBe('bge-m3');
    });

    it('exits 1 for non-existent key', async () => {
      writeConfig(tmpDir);
      await expect(run(['get', 'nonexistent.path'], tmpDir)).rejects.toThrow('process.exit(1)');
      expect(exitCode).toBe(1);
      expect(errors.some((e) => e.includes('Key not found'))).toBe(true);
    });

    it('exits 1 when no key provided', async () => {
      writeConfig(tmpDir);
      await expect(run(['get'], tmpDir)).rejects.toThrow('process.exit(1)');
      expect(exitCode).toBe(1);
    });
  });

  describe('config set', () => {
    // `myco config set` writes the project myco.yaml via updateConfig, so it
    // only persists project-tier fields. capture.* moved to Machine tier
    // (2026-06 scope correction), so these tests use a project-tier field
    // (release_provenance.github.max_lookups_per_run) to exercise nested-path
    // set + number coercion + intermediate-object creation.
    it('sets a nested value and persists to file', async () => {
      writeConfig(tmpDir);
      await run(['set', 'release_provenance.github.max_lookups_per_run', '50'], tmpDir);
      const config = readConfig(tmpDir);
      const rp = config.release_provenance as Record<string, unknown>;
      expect((rp.github as Record<string, unknown>).max_lookups_per_run).toBe(50);
      expect(logged.some((l) => l.includes('Set release_provenance.github.max_lookups_per_run'))).toBe(true);
    });

    it('coerces number values via JSON parse', async () => {
      writeConfig(tmpDir);
      await run(['set', 'release_provenance.github.max_lookups_per_run', '100'], tmpDir);
      const config = readConfig(tmpDir);
      const rp = config.release_provenance as Record<string, unknown>;
      expect((rp.github as Record<string, unknown>).max_lookups_per_run).toBe(100);
    });

    it('creates intermediate objects along dot-path', async () => {
      writeConfig(tmpDir);
      await run(['set', 'release_provenance.github.max_lookups_per_run', '21'], tmpDir);
      const config = readConfig(tmpDir);
      const rp = config.release_provenance as Record<string, unknown>;
      expect((rp.github as Record<string, unknown>).max_lookups_per_run).toBe(21);
    });

    it('exits 1 on Zod validation failure', async () => {
      writeConfig(tmpDir);
      // version must be literal 3, setting to 99 should fail
      await expect(run(['set', 'version', '99'], tmpDir)).rejects.toThrow('process.exit(1)');
      expect(exitCode).toBe(1);
      expect(errors.some((e) => e.includes('Validation error'))).toBe(true);
    });

    it('exits 1 when no value provided', async () => {
      writeConfig(tmpDir);
      await expect(run(['set', 'embedding.model'], tmpDir)).rejects.toThrow('process.exit(1)');
      expect(exitCode).toBe(1);
    });

    it('prints daemon restart notice when daemon.json exists', async () => {
      writeConfig(tmpDir);
      const statePath = resolveServiceDaemonStatePath();
      fs.mkdirSync(path.dirname(statePath), { recursive: true });
      fs.writeFileSync(statePath, '{}', 'utf-8');
      try {
        await run(['set', 'embedding.model', 'nomic-embed-text'], tmpDir);
        expect(logged.some((l) => l.includes('restart the daemon'))).toBe(true);
      } finally {
        try { fs.unlinkSync(statePath); } catch { /* gone */ }
      }
    });

    it('does not print daemon restart notice when daemon.json is absent', async () => {
      writeConfig(tmpDir);
      try { fs.unlinkSync(resolveServiceDaemonStatePath()); } catch { /* gone */ }
      await run(['set', 'embedding.model', 'nomic-embed-text'], tmpDir);
      expect(logged.every((l) => !l.includes('restart the daemon'))).toBe(true);
    });
  });

  describe('agent schema toggles', () => {
    const minimalConfig = { version: 3, embedding: { provider: 'ollama', model: 'bge-m3' } };

    it('scheduled_tasks_enabled defaults to true when not specified', () => {
      const config = MycoConfigSchema.parse(minimalConfig);
      expect(config.agent.scheduled_tasks_enabled).toBe(true);
    });

    it('event_tasks_enabled defaults to true when not specified', () => {
      const config = MycoConfigSchema.parse(minimalConfig);
      expect(config.agent.event_tasks_enabled).toBe(true);
    });

    it('scheduled_tasks_enabled can be set to false', () => {
      const config = MycoConfigSchema.parse({
        ...minimalConfig,
        agent: { scheduled_tasks_enabled: false },
      });
      expect(config.agent.scheduled_tasks_enabled).toBe(false);
    });
  });

  describe('agent task params', () => {
    it('agent.tasks supports params override', () => {
      const config = MycoConfigSchema.parse({
        version: 3,
        embedding: { provider: 'ollama', model: 'bge-m3' },
        agent: {
          tasks: {
            'skill-evolve': {
              params: { assess_interval_hours: 12, max_skills_per_run: 3 },
            },
          },
        },
      });
      expect(config.agent.tasks!['skill-evolve'].params).toEqual({
        assess_interval_hours: 12,
        max_skills_per_run: 3,
      });
    });
  });

  describe('symbionts schema', () => {
    it('parses symbionts map from config', () => {
      const config = MycoConfigSchema.parse({
        version: 3,
        symbionts: {
          'claude-code': { enabled: true },
          'cursor': { enabled: false },
        },
      });
      expect(config.symbionts).toEqual({
        'claude-code': { enabled: true },
        'cursor': { enabled: false },
      });
    });

    it('defaults symbionts to undefined when absent', () => {
      const config = MycoConfigSchema.parse({ version: 3 });
      expect(config.symbionts).toBeUndefined();
    });
  });

  describe('error handling', () => {
    it('errors on missing vault (no myco.yaml)', async () => {
      // loadConfig will throw since there's no myco.yaml
      await expect(run(['get', 'version'], tmpDir)).rejects.toThrow(/myco\.yaml not found/);
    });

    it('exits 1 with unknown subcommand', async () => {
      writeConfig(tmpDir);
      await expect(run(['unknown'], tmpDir)).rejects.toThrow('process.exit(1)');
      expect(exitCode).toBe(1);
    });
  });
});
