import fs from 'fs';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { stringify } from 'yaml';

import {
  cleanup,
  createTempDir,
  initGitRepo,
  setupNanoclawDir,
} from './test-helpers.js';

let tmpDir: string;
const originalCwd = process.cwd();

describe('update', () => {
  beforeEach(() => {
    tmpDir = createTempDir();
    setupNanoclawDir(tmpDir);
    initGitRepo(tmpDir);
    process.chdir(tmpDir);
  });

  afterEach(() => {
    process.chdir(originalCwd);
    cleanup(tmpDir);
  });

  function writeStateFile(state: Record<string, unknown>): void {
    const statePath = path.join(tmpDir, '.nanoclaw', 'state.yaml');
    fs.writeFileSync(statePath, stringify(state), 'utf-8');
  }

  function createNewCoreDir(files: Record<string, string>): string {
    const newCoreDir = path.join(tmpDir, 'new-core');
    fs.mkdirSync(newCoreDir, { recursive: true });

    for (const [relPath, content] of Object.entries(files)) {
      const fullPath = path.join(newCoreDir, relPath);
      fs.mkdirSync(path.dirname(fullPath), { recursive: true });
      fs.writeFileSync(fullPath, content);
    }

    return newCoreDir;
  }

  describe('previewUpdate', () => {
    it('detects new files in update', async () => {
      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      const newCoreDir = createNewCoreDir({
        'src/new-file.ts': 'export const x = 1;',
      });

      const { previewUpdate } = await import('../update.js');
      const preview = previewUpdate(newCoreDir);

      expect(preview.filesChanged).toContain('src/new-file.ts');
      expect(preview.currentVersion).toBe('1.0.0');
    });

    it('detects changed files vs base', async () => {
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(baseDir, 'src/index.ts'), 'original');

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'modified',
      });

      const { previewUpdate } = await import('../update.js');
      const preview = previewUpdate(newCoreDir);

      expect(preview.filesChanged).toContain('src/index.ts');
    });

    it('does not list unchanged files', async () => {
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(baseDir, 'src/index.ts'), 'same content');

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'same content',
      });

      const { previewUpdate } = await import('../update.js');
      const preview = previewUpdate(newCoreDir);

      expect(preview.filesChanged).not.toContain('src/index.ts');
    });

    it('identifies conflict risk with applied skills', async () => {
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(baseDir, 'src/index.ts'), 'original');

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [
          {
            name: 'telegram',
            version: '1.0.0',
            applied_at: new Date().toISOString(),
            file_hashes: { 'src/index.ts': 'abc123' },
          },
        ],
      });

      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'updated core',
      });

      const { previewUpdate } = await import('../update.js');
      const preview = previewUpdate(newCoreDir);

      expect(preview.conflictRisk).toContain('src/index.ts');
    });

    it('identifies custom patches at risk', async () => {
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(baseDir, 'src/config.ts'), 'original');

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
        custom_modifications: [
          {
            description: 'custom tweak',
            applied_at: new Date().toISOString(),
            files_modified: ['src/config.ts'],
            patch_file: '.nanoclaw/custom/001-tweak.patch',
          },
        ],
      });

      const newCoreDir = createNewCoreDir({
        'src/config.ts': 'updated core config',
      });

      const { previewUpdate } = await import('../update.js');
      const preview = previewUpdate(newCoreDir);

      expect(preview.customPatchesAtRisk).toContain('src/config.ts');
    });

    it('reads version from package.json in new core', async () => {
      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      const newCoreDir = createNewCoreDir({
        'package.json': JSON.stringify({ version: '2.0.0' }),
      });

      const { previewUpdate } = await import('../update.js');
      const preview = previewUpdate(newCoreDir);

      expect(preview.newVersion).toBe('2.0.0');
    });

    it('detects files deleted in new core', async () => {
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(baseDir, 'src/index.ts'), 'keep this');
      fs.writeFileSync(path.join(baseDir, 'src/removed.ts'), 'delete this');

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      // New core only has index.ts — removed.ts is gone
      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'keep this',
      });

      const { previewUpdate } = await import('../update.js');
      const preview = previewUpdate(newCoreDir);

      expect(preview.filesDeleted).toContain('src/removed.ts');
      expect(preview.filesChanged).not.toContain('src/removed.ts');
    });
  });

  describe('applyUpdate', () => {
    it('rejects when customize session is active', async () => {
      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      // Create the pending.yaml that indicates active customize
      const customDir = path.join(tmpDir, '.nanoclaw', 'custom');
      fs.mkdirSync(customDir, { recursive: true });
      fs.writeFileSync(path.join(customDir, 'pending.yaml'), 'active: true');

      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'new content',
      });

      const { applyUpdate } = await import('../update.js');
      const result = await applyUpdate(newCoreDir);

      expect(result.success).toBe(false);
      expect(result.error).toContain('customize session');
    });

    it('copies new files that do not exist yet', async () => {
      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      const newCoreDir = createNewCoreDir({
        'src/brand-new.ts': 'export const fresh = true;',
      });

      const { applyUpdate } = await import('../update.js');
      const result = await applyUpdate(newCoreDir);

      expect(result.error).toBeUndefined();
      expect(result.success).toBe(true);
      expect(
        fs.readFileSync(path.join(tmpDir, 'src/brand-new.ts'), 'utf-8'),
      ).toBe('export const fresh = true;');
    });

    it('performs clean three-way merge', async () => {
      // Set up base
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(
        path.join(baseDir, 'src/index.ts'),
        'line 1\nline 2\nline 3\n',
      );

      // Current has user changes at the bottom
      fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
      fs.writeFileSync(
        path.join(tmpDir, 'src/index.ts'),
        'line 1\nline 2\nline 3\nuser addition\n',
      );

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      // New core changes at the top
      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'core update\nline 1\nline 2\nline 3\n',
        'package.json': JSON.stringify({ version: '2.0.0' }),
      });

      const { applyUpdate } = await import('../update.js');
      const result = await applyUpdate(newCoreDir);

      expect(result.success).toBe(true);
      expect(result.newVersion).toBe('2.0.0');

      const merged = fs.readFileSync(
        path.join(tmpDir, 'src/index.ts'),
        'utf-8',
      );
      expect(merged).toContain('core update');
      expect(merged).toContain('user addition');
    });

    it('updates base directory after successful merge', async () => {
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(baseDir, 'src/index.ts'), 'old base');

      fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(tmpDir, 'src/index.ts'), 'old base');

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'new base content',
      });

      const { applyUpdate } = await import('../update.js');
      await applyUpdate(newCoreDir);

      const newBase = fs.readFileSync(
        path.join(tmpDir, '.nanoclaw', 'base', 'src/index.ts'),
        'utf-8',
      );
      expect(newBase).toBe('new base content');
    });

    it('updates core_version in state after success', async () => {
      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      const newCoreDir = createNewCoreDir({
        'package.json': JSON.stringify({ version: '2.0.0' }),
      });

      const { applyUpdate } = await import('../update.js');
      const result = await applyUpdate(newCoreDir);

      expect(result.success).toBe(true);
      expect(result.previousVersion).toBe('1.0.0');
      expect(result.newVersion).toBe('2.0.0');

      // Verify state file was updated
      const { readState } = await import('../state.js');
      const state = readState();
      expect(state.core_version).toBe('2.0.0');
    });

    it('restores backup on merge conflict', async () => {
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(
        path.join(baseDir, 'src/index.ts'),
        'line 1\nline 2\nline 3\n',
      );

      // Current has conflicting change on same line
      fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
      fs.writeFileSync(
        path.join(tmpDir, 'src/index.ts'),
        'line 1\nuser changed line 2\nline 3\n',
      );

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      // New core also changes line 2 — guaranteed conflict
      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'line 1\ncore changed line 2\nline 3\n',
      });

      const { applyUpdate } = await import('../update.js');
      const result = await applyUpdate(newCoreDir);

      expect(result.success).toBe(false);
      expect(result.mergeConflicts).toContain('src/index.ts');
      expect(result.backupPending).toBe(true);

      // File should have conflict markers (backup preserved, not restored)
      const content = fs.readFileSync(
        path.join(tmpDir, 'src/index.ts'),
        'utf-8',
      );
      expect(content).toContain('<<<<<<<');
      expect(content).toContain('>>>>>>>');
    });

    it('removes files deleted in new core', async () => {
      const baseDir = path.join(tmpDir, '.nanoclaw', 'base');
      fs.mkdirSync(path.join(baseDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(baseDir, 'src/index.ts'), 'keep');
      fs.writeFileSync(path.join(baseDir, 'src/removed.ts'), 'old content');

      // Working tree has both files
      fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
      fs.writeFileSync(path.join(tmpDir, 'src/index.ts'), 'keep');
      fs.writeFileSync(path.join(tmpDir, 'src/removed.ts'), 'old content');

      writeStateFile({
        skills_system_version: '0.1.0',
        core_version: '1.0.0',
        applied_skills: [],
      });

      // New core only has index.ts
      const newCoreDir = createNewCoreDir({
        'src/index.ts': 'keep',
      });

      const { applyUpdate } = await import('../update.js');
      const result = await applyUpdate(newCoreDir);

      expect(result.success).toBe(true);
      expect(fs.existsSync(path.join(tmpDir, 'src/index.ts'))).toBe(true);
      expect(fs.existsSync(path.join(tmpDir, 'src/removed.ts'))).toBe(false);
    });
  });
});
