import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { execSync } from 'child_process';

/**
 * Tests for the sync core logic.
 * We test the git helper functions and sync state management
 * by operating on a real temp git repo.
 *
 * Note: The sync API route (/api/sync) reads ~/.mindos/config.json directly,
 * making it hard to unit test without modifying global state.
 * These tests cover the underlying git operations instead.
 */

let tmpDir: string;
let mindRoot: string;

function setup() {
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mindos-sync-test-'));
  mindRoot = path.join(tmpDir, 'mind');
  fs.mkdirSync(mindRoot, { recursive: true });
  execSync('git init', { cwd: mindRoot, stdio: 'pipe' });
  execSync('git checkout -b main', { cwd: mindRoot, stdio: 'pipe' });
  execSync('git config user.email "test@test.com"', { cwd: mindRoot, stdio: 'pipe' });
  execSync('git config user.name "Test"', { cwd: mindRoot, stdio: 'pipe' });
  fs.writeFileSync(path.join(mindRoot, 'README.md'), '# test\n');
  execSync('git add -A && git commit -m "init"', { cwd: mindRoot, stdio: 'pipe' });
}

function cleanup() {
  if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
}

describe('sync: git operations', () => {
  beforeEach(setup);
  afterEach(cleanup);

  it('detects git repo correctly', () => {
    expect(fs.existsSync(path.join(mindRoot, '.git'))).toBe(true);
    expect(fs.existsSync(path.join(tmpDir, '.git'))).toBe(false);
  });

  it('reads branch name', () => {
    const branch = execSync('git rev-parse --abbrev-ref HEAD', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    expect(branch).toBe('main');
  });

  it('detects no uncommitted changes after clean commit', () => {
    const status = execSync('git status --porcelain', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    expect(status).toBe('');
  });

  it('detects uncommitted changes', () => {
    fs.writeFileSync(path.join(mindRoot, 'new-file.md'), 'hello\n');
    const status = execSync('git status --porcelain', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    expect(status).toContain('new-file.md');
  });

  it('auto-commit stages and commits changes', () => {
    fs.writeFileSync(path.join(mindRoot, 'auto-test.md'), 'auto content\n');
    execSync('git add -A', { cwd: mindRoot, stdio: 'pipe' });
    const status = execSync('git status --porcelain', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    expect(status).not.toBe('');

    const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
    execSync(`git commit -m "auto-sync: ${timestamp}"`, { cwd: mindRoot, stdio: 'pipe' });

    const afterStatus = execSync('git status --porcelain', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    expect(afterStatus).toBe('');

    const log = execSync('git log --oneline -1', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    expect(log).toContain('auto-sync:');
  });

  it('handles already-clean repo gracefully (no empty commits)', () => {
    const statusBefore = execSync('git status --porcelain', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    expect(statusBefore).toBe('');

    // Count commits before
    const countBefore = execSync('git rev-list --count HEAD', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();

    // Simulate autoCommitAndPush skipping when no changes
    execSync('git add -A', { cwd: mindRoot, stdio: 'pipe' });
    const status = execSync('git status --porcelain', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    // Should be empty — no commit needed
    expect(status).toBe('');

    const countAfter = execSync('git rev-list --count HEAD', {
      cwd: mindRoot, encoding: 'utf-8', stdio: 'pipe',
    }).trim();
    expect(countAfter).toBe(countBefore);
  });
});

describe('sync: conflict file creation', () => {
  beforeEach(setup);
  afterEach(cleanup);

  it('creates .sync-conflict file with theirs content', () => {
    // Simulate what autoPull conflict resolution does
    const file = 'notes.md';
    const localContent = 'local version\n';
    const remoteContent = 'remote version\n';

    fs.writeFileSync(path.join(mindRoot, file), localContent);
    fs.writeFileSync(path.join(mindRoot, file + '.sync-conflict'), remoteContent);

    expect(fs.readFileSync(path.join(mindRoot, file), 'utf-8')).toBe(localContent);
    expect(fs.readFileSync(path.join(mindRoot, file + '.sync-conflict'), 'utf-8')).toBe(remoteContent);
  });
});

describe('sync: state file management', () => {
  beforeEach(setup);
  afterEach(cleanup);

  it('reads and writes sync state', () => {
    const statePath = path.join(tmpDir, 'sync-state.json');

    // Initially no state file
    expect(fs.existsSync(statePath)).toBe(false);

    // Write state
    const state = {
      lastSync: new Date().toISOString(),
      lastPull: new Date().toISOString(),
      conflicts: [],
      lastError: null,
    };
    fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');

    // Read back
    const loaded = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
    expect(loaded.lastSync).toBe(state.lastSync);
    expect(loaded.lastPull).toBe(state.lastPull);
    expect(loaded.conflicts).toEqual([]);
    expect(loaded.lastError).toBeNull();
  });

  it('stores conflict records correctly', () => {
    const statePath = path.join(tmpDir, 'sync-state.json');
    const conflicts = [
      { file: 'notes/todo.md', time: '2026-03-14T10:30:00.000Z' },
      { file: 'journal/2026-03.md', time: '2026-03-14T10:30:00.000Z' },
    ];
    const state = { lastSync: new Date().toISOString(), conflicts };
    fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');

    const loaded = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
    expect(loaded.conflicts).toHaveLength(2);
    expect(loaded.conflicts[0].file).toBe('notes/todo.md');
    expect(loaded.conflicts[1].file).toBe('journal/2026-03.md');
  });
});

describe('sync: .gitignore auto-creation (O5)', () => {
  beforeEach(setup);
  afterEach(cleanup);

  it('creates .gitignore with expected entries when file does not exist', () => {
    const gitignorePath = path.join(mindRoot, '.gitignore');

    // Initially setup() does NOT create .gitignore
    expect(fs.existsSync(gitignorePath)).toBe(false);

    // Simulate what initSync does: create .gitignore if missing
    if (!fs.existsSync(gitignorePath)) {
      fs.writeFileSync(gitignorePath, [
        '# MindOS auto-generated',
        '.DS_Store',
        'Thumbs.db',
        '*.tmp',
        '*.bak',
        '*.swp',
        '*.sync-conflict',
        'node_modules/',
        '.obsidian/',
        '',
      ].join('\n'), 'utf-8');
    }

    expect(fs.existsSync(gitignorePath)).toBe(true);
    const content = fs.readFileSync(gitignorePath, 'utf-8');
    expect(content).toContain('.DS_Store');
    expect(content).toContain('*.sync-conflict');
    expect(content).toContain('node_modules/');
    expect(content).toContain('.obsidian/');
  });

  it('does not overwrite existing .gitignore', () => {
    const gitignorePath = path.join(mindRoot, '.gitignore');
    const customContent = '# Custom gitignore\n*.log\n';
    fs.writeFileSync(gitignorePath, customContent);

    // Simulate initSync check: skip if exists
    if (!fs.existsSync(gitignorePath)) {
      fs.writeFileSync(gitignorePath, '# MindOS auto-generated\n');
    }

    expect(fs.readFileSync(gitignorePath, 'utf-8')).toBe(customContent);
  });
});

describe('sync: daemon idempotency (O1)', () => {
  it('idempotent guard prevents double activation', () => {
    // Simulate the idempotent guard logic from startSyncDaemon
    let activeWatcher: unknown = null;
    let callCount = 0;

    function startDaemon() {
      if (activeWatcher) return null; // idempotent guard
      callCount++;
      activeWatcher = { close() { activeWatcher = null; } };
      return activeWatcher;
    }

    // First call succeeds
    const result1 = startDaemon();
    expect(result1).not.toBeNull();
    expect(callCount).toBe(1);

    // Second call returns null (already running)
    const result2 = startDaemon();
    expect(result2).toBeNull();
    expect(callCount).toBe(1); // not incremented
  });
});

describe('sync: manualSync throws instead of process.exit (bonus)', () => {
  it('throws error for non-git directory', () => {
    // Simulate the manualSync logic: throw instead of process.exit
    function manualSync(mindRoot: string | null) {
      if (!mindRoot || !fs.existsSync(path.join(mindRoot, '.git'))) {
        throw new Error('Not a git repository. Run `mindos sync init` first.');
      }
    }

    // Should throw for a directory without .git
    expect(() => manualSync(os.tmpdir())).toThrow('Not a git repository');
    expect(() => manualSync(null)).toThrow('Not a git repository');
  });
});

describe('sync: config management', () => {
  beforeEach(setup);
  afterEach(cleanup);

  it('merges sync config into existing config', () => {
    const configPath = path.join(tmpDir, 'config.json');
    const existing = {
      mindRoot,
      port: 3456,
      authToken: 'test',
    };
    fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));

    // Add sync config
    const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    config.sync = {
      enabled: true,
      provider: 'git',
      remote: 'origin',
      branch: 'main',
      autoCommitInterval: 30,
      autoPullInterval: 300,
    };
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2));

    // Verify
    const reloaded = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    expect(reloaded.mindRoot).toBe(mindRoot);
    expect(reloaded.authToken).toBe('test');
    expect(reloaded.sync.enabled).toBe(true);
    expect(reloaded.sync.autoCommitInterval).toBe(30);
    expect(reloaded.sync.autoPullInterval).toBe(300);
  });

  it('toggles sync enabled flag without losing other config', () => {
    const configPath = path.join(tmpDir, 'config.json');
    const config = {
      mindRoot,
      port: 3456,
      sync: { enabled: true, provider: 'git', autoCommitInterval: 30, autoPullInterval: 300 },
    };
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2));

    // Disable
    const loaded = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    loaded.sync.enabled = false;
    fs.writeFileSync(configPath, JSON.stringify(loaded, null, 2));

    const reloaded = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    expect(reloaded.sync.enabled).toBe(false);
    expect(reloaded.sync.provider).toBe('git');
    expect(reloaded.mindRoot).toBe(mindRoot);
  });
});

describe('sync: path traversal protection', () => {
  beforeEach(setup);
  afterEach(cleanup);

  /**
   * Helper to test if a file path is safely within mindRoot
   * (mirrors the isPathWithinMindRoot function in sync/route.ts)
   */
  function isPathWithinMindRoot(mindRootDir: string, filePath: string): boolean {
    const normalizedPath = path.resolve(mindRootDir, filePath);
    return normalizedPath.startsWith(mindRootDir + '/') || normalizedPath === mindRootDir;
  }

  it('allows normal relative paths', () => {
    expect(isPathWithinMindRoot(mindRoot, 'notes/todo.md')).toBe(true);
    expect(isPathWithinMindRoot(mindRoot, 'README.md')).toBe(true);
    expect(isPathWithinMindRoot(mindRoot, 'deep/nested/file.md')).toBe(true);
  });

  it('blocks path traversal with ../', () => {
    expect(isPathWithinMindRoot(mindRoot, '../etc/passwd')).toBe(false);
    expect(isPathWithinMindRoot(mindRoot, '../../etc/passwd')).toBe(false);
    expect(isPathWithinMindRoot(mindRoot, 'notes/../../../etc/passwd')).toBe(false);
  });

  it('blocks absolute paths outside mindRoot', () => {
    expect(isPathWithinMindRoot(mindRoot, '/etc/passwd')).toBe(false);
    expect(isPathWithinMindRoot(mindRoot, '/tmp/malicious')).toBe(false);
  });

  it('allows paths that start with .. but resolve within mindRoot', () => {
    // e.g., notes/../README.md resolves to mindRoot/README.md
    expect(isPathWithinMindRoot(mindRoot, 'notes/../README.md')).toBe(true);
    expect(isPathWithinMindRoot(mindRoot, './notes/../notes/todo.md')).toBe(true);
  });
});
