#!/usr/bin/env node
/**
 * Tests for Story 16.5: Fix Config Lost on Update
 * Tests readExistingLayout() and collectRepoLayout() with existingLayout parameter
 */
'use strict';

const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');

let passed = 0;
let failed = 0;
const errors = [];

function test(name, fn) {
  try {
    fn();
    console.log(`  \u2713 ${name}`);
    passed++;
  } catch (err) {
    console.error(`  \u2717 ${name}: ${err.message}`);
    failed++;
    errors.push({ name, error: err.message });
  }
}

async function asyncTest(name, fn) {
  try {
    await fn();
    console.log(`  \u2713 ${name}`);
    passed++;
  } catch (err) {
    console.error(`  \u2717 ${name}: ${err.message}`);
    failed++;
    errors.push({ name, error: err.message });
  }
}

// ─── Load module ─────────────────────────────────────────────────────────────
let readExistingLayout, collectRepoLayout, writeProjectLayoutYaml;
try {
  ({ readExistingLayout, collectRepoLayout, writeProjectLayoutYaml } = require('../bin/cli.js'));
} catch (e) {
  console.error('\n  FATAL: Cannot load exports from cli.js');
  console.error(' ', e.message);
  process.exit(1);
}

// Helper: create temp dir and set cwd, returns cleanup fn
function withTmpDir() {
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-test-'));
  const origCwd = process.cwd();
  process.chdir(tmpDir);
  return {
    tmpDir,
    cleanup() {
      process.chdir(origCwd);
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  };
}

// Helper: write a project-layout.yaml
function writeLayoutFile(tmpDir, content) {
  const dir = path.join(tmpDir, '_bmad-output');
  fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(path.join(dir, 'project-layout.yaml'), content, 'utf-8');
}

// Helper: write a config.yaml with knowledgebase/sprint paths
function writeConfigFile(tmpDir, content) {
  const dir = path.join(tmpDir, '_bmad', 'bmm');
  fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(path.join(dir, 'config.yaml'), content, 'utf-8');
}

// ─── readExistingLayout — project-layout.yaml ──────────────────────────────
console.log('\nreadExistingLayout (project-layout.yaml)');

test('returns correct layout from valid project-layout.yaml', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    writeLayoutFile(tmpDir, [
      '# Generated by ma-agents',
      'generated: "2026-03-28"',
      'knowledgebase:',
      '  mode: local',
      '  path: "/ext/kb"',
      'sprint_management:',
      '  mode: same',
      '  path: "."',
    ].join('\n'));

    const result = readExistingLayout();
    assert.ok(result, 'should return layout object');
    assert.strictEqual(result.knowledgebase.mode, 'local');
    assert.strictEqual(result.knowledgebase.path, '/ext/kb');
    assert.strictEqual(result.sprintManagement.mode, 'same');
    assert.strictEqual(result.sprintManagement.path, '.');
  } finally {
    cleanup();
  }
});

test('parses remote mode with gitUrl', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    writeLayoutFile(tmpDir, [
      'knowledgebase:',
      '  mode: remote',
      '  path: "/tmp/kb"',
      '  gitUrl: "https://git.example.com/kb.git"',
      'sprint_management:',
      '  mode: local',
      '  path: "/tmp/sprint"',
    ].join('\n'));

    const result = readExistingLayout();
    assert.ok(result, 'should return layout');
    assert.strictEqual(result.knowledgebase.mode, 'remote');
    assert.strictEqual(result.knowledgebase.gitUrl, 'https://git.example.com/kb.git');
    assert.strictEqual(result.sprintManagement.mode, 'local');
    assert.strictEqual(result.sprintManagement.path, '/tmp/sprint');
  } finally {
    cleanup();
  }
});

test('returns null when no project-layout.yaml exists', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    // No files at all
    const result = readExistingLayout();
    assert.strictEqual(result, null, 'should return null when no layout file');
  } finally {
    cleanup();
  }
});

test('returns null on corrupt YAML (no crash)', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    writeLayoutFile(tmpDir, '{{{{invalid yaml garbage\n\x00\x01\x02');
    const result = readExistingLayout();
    assert.strictEqual(result, null, 'should return null on corrupt file');
  } finally {
    cleanup();
  }
});

test('fills in default for missing section', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    // Only knowledgebase section, no sprint_management
    writeLayoutFile(tmpDir, [
      'knowledgebase:',
      '  mode: local',
      '  path: "/ext/kb"',
    ].join('\n'));

    const result = readExistingLayout();
    assert.ok(result, 'should return layout');
    assert.strictEqual(result.knowledgebase.mode, 'local');
    assert.strictEqual(result.sprintManagement.mode, 'same', 'missing section defaults to same');
    assert.strictEqual(result.sprintManagement.path, '.', 'missing section defaults to "."');
  } finally {
    cleanup();
  }
});

// ─── readExistingLayout — config.yaml fallback ─────────────────────────────
console.log('\nreadExistingLayout (config.yaml fallback)');

test('falls back to config.yaml when no project-layout.yaml', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    writeConfigFile(tmpDir, [
      '# BMM Config',
      'knowledgebase_path: "/ext/kb"',
      'sprint_management_path: "."',
    ].join('\n'));

    const result = readExistingLayout();
    assert.ok(result, 'should return layout from config.yaml fallback');
    assert.strictEqual(result.knowledgebase.mode, 'local');
    assert.strictEqual(result.knowledgebase.path, '/ext/kb');
    assert.strictEqual(result.sprintManagement.mode, 'same');
    assert.strictEqual(result.sprintManagement.path, '.');
  } finally {
    cleanup();
  }
});

test('returns null from config.yaml when both paths are "."', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    writeConfigFile(tmpDir, [
      'knowledgebase_path: "."',
      'sprint_management_path: "."',
    ].join('\n'));

    const result = readExistingLayout();
    assert.strictEqual(result, null, 'all-default config should return null');
  } finally {
    cleanup();
  }
});

test('prefers project-layout.yaml over config.yaml', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    writeLayoutFile(tmpDir, [
      'knowledgebase:',
      '  mode: local',
      '  path: "/layout-path"',
      'sprint_management:',
      '  mode: same',
      '  path: "."',
    ].join('\n'));
    writeConfigFile(tmpDir, [
      'knowledgebase_path: "/config-path"',
      'sprint_management_path: "."',
    ].join('\n'));

    const result = readExistingLayout();
    assert.ok(result, 'should return layout');
    assert.strictEqual(result.knowledgebase.path, '/layout-path',
      'project-layout.yaml should take precedence over config.yaml');
  } finally {
    cleanup();
  }
});

// ─── collectRepoLayout with existingLayout (--yes mode) ────────────────────
console.log('\ncollectRepoLayout with existingLayout (--yes mode)');

asyncTest('--yes preserves existing layout when no env vars', async () => {
  const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
  const origSp = process.env.MA_SPRINT_PATH;
  try {
    delete process.env.MA_KNOWLEDGEBASE_PATH;
    delete process.env.MA_SPRINT_PATH;

    const existing = {
      knowledgebase: { mode: 'local', path: '/preserved/kb' },
      sprintManagement: { mode: 'local', path: '/preserved/sprint' },
    };

    const result = await collectRepoLayout({ yes: true }, existing);
    assert.strictEqual(result.knowledgebase.path, '/preserved/kb', 'should preserve kb path');
    assert.strictEqual(result.sprintManagement.path, '/preserved/sprint', 'should preserve sprint path');
  } finally {
    if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
    else delete process.env.MA_KNOWLEDGEBASE_PATH;
    if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
    else delete process.env.MA_SPRINT_PATH;
  }
});

asyncTest('--yes with env vars overrides existing layout', async () => {
  const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
  const origSp = process.env.MA_SPRINT_PATH;
  try {
    process.env.MA_KNOWLEDGEBASE_PATH = '/env/kb';
    process.env.MA_SPRINT_PATH = '/env/sprint';

    const existing = {
      knowledgebase: { mode: 'local', path: '/old/kb' },
      sprintManagement: { mode: 'local', path: '/old/sprint' },
    };

    const result = await collectRepoLayout({ yes: true }, existing);
    assert.strictEqual(result.knowledgebase.path, path.resolve('/env/kb'), 'env var should override kb');
    assert.strictEqual(result.sprintManagement.path, path.resolve('/env/sprint'), 'env var should override sprint');
  } finally {
    if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
    else delete process.env.MA_KNOWLEDGEBASE_PATH;
    if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
    else delete process.env.MA_SPRINT_PATH;
  }
});

asyncTest('--yes with no env vars and no existing layout defaults to single-repo', async () => {
  const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
  const origSp = process.env.MA_SPRINT_PATH;
  try {
    delete process.env.MA_KNOWLEDGEBASE_PATH;
    delete process.env.MA_SPRINT_PATH;

    const result = await collectRepoLayout({ yes: true }, null);
    assert.strictEqual(result.knowledgebase.mode, 'same');
    assert.strictEqual(result.knowledgebase.path, '.');
    assert.strictEqual(result.sprintManagement.mode, 'same');
    assert.strictEqual(result.sprintManagement.path, '.');
  } finally {
    if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
    else delete process.env.MA_KNOWLEDGEBASE_PATH;
    if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
    else delete process.env.MA_SPRINT_PATH;
  }
});

asyncTest('--yes with partial env vars preserves non-overridden concern from existing', async () => {
  const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
  const origSp = process.env.MA_SPRINT_PATH;
  try {
    process.env.MA_KNOWLEDGEBASE_PATH = '/env/kb';
    delete process.env.MA_SPRINT_PATH;

    const existing = {
      knowledgebase: { mode: 'local', path: '/old/kb' },
      sprintManagement: { mode: 'local', path: '/existing/sprint' },
    };

    const result = await collectRepoLayout({ yes: true }, existing);
    assert.strictEqual(result.knowledgebase.path, path.resolve('/env/kb'), 'env var overrides kb');
    assert.strictEqual(result.sprintManagement.path, '/existing/sprint', 'existing sprint preserved');
  } finally {
    if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
    else delete process.env.MA_KNOWLEDGEBASE_PATH;
    if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
    else delete process.env.MA_SPRINT_PATH;
  }
});

// ─── Round-trip: writeProjectLayoutYaml → readExistingLayout ───────────────
console.log('\nRound-trip: write then read');

test('readExistingLayout correctly reads what writeProjectLayoutYaml writes', () => {
  const { tmpDir, cleanup } = withTmpDir();
  try {
    const layout = {
      knowledgebase: { mode: 'local', path: 'd:\\Code\\kb-repo' },
      sprintManagement: { mode: 'remote', path: '/tmp/sprint', gitUrl: 'https://git.example.com/sprint.git' }
    };
    writeProjectLayoutYaml(layout);

    const result = readExistingLayout();
    assert.ok(result, 'should read back layout');
    assert.strictEqual(result.knowledgebase.mode, 'local');
    assert.strictEqual(result.knowledgebase.path, 'd:/Code/kb-repo', 'path should be normalized');
    assert.strictEqual(result.sprintManagement.mode, 'remote');
    assert.strictEqual(result.sprintManagement.gitUrl, 'https://git.example.com/sprint.git');
  } finally {
    cleanup();
  }
});

// ─── Summary ───────────────────────────────────────────────────────────────
async function runAll() {
  // Wait for any remaining async tests
  await new Promise(resolve => setTimeout(resolve, 100));
  console.log(`\n${passed} passed, ${failed} failed`);
  if (errors.length > 0) {
    console.log('\nFailed tests:');
    errors.forEach(e => console.log(`  - ${e.name}: ${e.error}`));
  }
  if (failed > 0) process.exit(1);
}

runAll();
