#!/usr/bin/env node
/**
 * UUID Persistence Tests
 *
 * Simulates the exact conditions that caused character data loss:
 * - Config file corruption during updates
 * - File locks making config temporarily unreadable
 * - Username re-entry after config wipe
 *
 * Run: node test-uuid-persistence.js
 */

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

// Use a temp directory so we don't mess with real config
const TEST_DIR = path.join(os.tmpdir(), 'hytale-uuid-test-' + Date.now());
const CONFIG_FILE = path.join(TEST_DIR, 'config.json');
const CONFIG_BACKUP = path.join(TEST_DIR, 'config.json.bak');
const CONFIG_TEMP = path.join(TEST_DIR, 'config.json.tmp');
const UUID_STORE_FILE = path.join(TEST_DIR, 'uuid-store.json');

// Track test results
let passed = 0;
let failed = 0;
const failures = [];

function assert(condition, message) {
  if (condition) {
    passed++;
    console.log(`  ✓ ${message}`);
  } else {
    failed++;
    failures.push(message);
    console.log(`  ✗ FAIL: ${message}`);
  }
}

function assertEqual(actual, expected, message) {
  if (actual === expected) {
    passed++;
    console.log(`  ✓ ${message}`);
  } else {
    failed++;
    failures.push(`${message} (expected: ${expected}, got: ${actual})`);
    console.log(`  ✗ FAIL: ${message} (expected: "${expected}", got: "${actual}")`);
  }
}

function cleanup() {
  try {
    if (fs.existsSync(TEST_DIR)) {
      fs.rmSync(TEST_DIR, { recursive: true });
    }
  } catch (e) {}
}

function setup() {
  cleanup();
  fs.mkdirSync(TEST_DIR, { recursive: true });
}

// ============================================================================
// Inline the config functions so we can override paths
// (We can't require config.js directly because it uses hardcoded getAppDir())
// ============================================================================

function validateConfig(config) {
  if (!config || typeof config !== 'object') return false;
  if (config.userUuids !== undefined && typeof config.userUuids !== 'object') return false;
  if (config.username !== undefined && (typeof config.username !== 'string')) return false;
  return true;
}

function loadConfig() {
  try {
    if (fs.existsSync(CONFIG_FILE)) {
      const data = fs.readFileSync(CONFIG_FILE, 'utf8');
      if (data.trim()) {
        const config = JSON.parse(data);
        if (validateConfig(config)) return config;
        console.warn('[Config] Primary config invalid structure, trying backup...');
      }
    }
  } catch (err) {
    console.error('[Config] Failed to load primary config:', err.message);
  }

  try {
    if (fs.existsSync(CONFIG_BACKUP)) {
      const data = fs.readFileSync(CONFIG_BACKUP, 'utf8');
      if (data.trim()) {
        const config = JSON.parse(data);
        if (validateConfig(config)) {
          console.log('[Config] Recovered from backup successfully');
          try { fs.writeFileSync(CONFIG_FILE, data, 'utf8'); } catch (e) {}
          return config;
        }
      }
    }
  } catch (err) {}

  return {};
}

function saveConfig(update) {
  const maxRetries = 3;
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      if (!fs.existsSync(TEST_DIR)) fs.mkdirSync(TEST_DIR, { recursive: true });

      const currentConfig = loadConfig();

      // SAFETY CHECK: refuse to save if file exists but loaded empty
      if (Object.keys(currentConfig).length === 0 && fs.existsSync(CONFIG_FILE)) {
        const fileSize = fs.statSync(CONFIG_FILE).size;
        if (fileSize > 2) {
          console.error(`[Config] REFUSING to save — loaded empty but file exists (${fileSize} bytes). Retrying...`);
          const delay = attempt * 50; // shorter delay for tests
          const start = Date.now();
          while (Date.now() - start < delay) {}
          continue;
        }
      }

      const newConfig = { ...currentConfig, ...update };
      const data = JSON.stringify(newConfig, null, 2);

      fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
      const verification = JSON.parse(fs.readFileSync(CONFIG_TEMP, 'utf8'));
      if (!validateConfig(verification)) throw new Error('Validation failed');

      if (fs.existsSync(CONFIG_FILE)) {
        try {
          const currentData = fs.readFileSync(CONFIG_FILE, 'utf8');
          if (currentData.trim()) fs.writeFileSync(CONFIG_BACKUP, currentData, 'utf8');
        } catch (e) {}
      }

      fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
      return true;
    } catch (err) {
      try { if (fs.existsSync(CONFIG_TEMP)) fs.unlinkSync(CONFIG_TEMP); } catch (e) {}
      if (attempt >= maxRetries) throw err;
    }
  }
}

function loadUuidStore() {
  try {
    if (fs.existsSync(UUID_STORE_FILE)) {
      const data = fs.readFileSync(UUID_STORE_FILE, 'utf8');
      if (data.trim()) return JSON.parse(data);
    }
  } catch (err) {}
  return {};
}

function saveUuidStore(store) {
  const tmpFile = UUID_STORE_FILE + '.tmp';
  fs.writeFileSync(tmpFile, JSON.stringify(store, null, 2), 'utf8');
  fs.renameSync(tmpFile, UUID_STORE_FILE);
}

function migrateUuidStoreIfNeeded() {
  if (fs.existsSync(UUID_STORE_FILE)) return;
  const config = loadConfig();
  if (config.userUuids && Object.keys(config.userUuids).length > 0) {
    console.log('[UUID Store] Migrating', Object.keys(config.userUuids).length, 'UUIDs');
    saveUuidStore(config.userUuids);
  }
}

function getUuidForUser(username) {
  const { v4: uuidv4 } = require('uuid');
  if (!username || !username.trim()) throw new Error('Username required');

  const displayName = username.trim();
  const normalizedLookup = displayName.toLowerCase();

  migrateUuidStoreIfNeeded();

  // 1. Check UUID store (source of truth)
  const uuidStore = loadUuidStore();
  const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === normalizedLookup);
  if (storeKey) {
    const existingUuid = uuidStore[storeKey];
    if (storeKey !== displayName) {
      delete uuidStore[storeKey];
      uuidStore[displayName] = existingUuid;
      saveUuidStore(uuidStore);
    }
    // Sync to config (non-critical)
    try {
      const config = loadConfig();
      const configUuids = config.userUuids || {};
      const configKey = Object.keys(configUuids).find(k => k.toLowerCase() === normalizedLookup);
      if (!configKey || configUuids[configKey] !== existingUuid) {
        if (configKey) delete configUuids[configKey];
        configUuids[displayName] = existingUuid;
        saveConfig({ userUuids: configUuids });
      }
    } catch (e) {}
    return existingUuid;
  }

  // 2. Fallback: check config.json
  const config = loadConfig();
  const userUuids = config.userUuids || {};
  const configKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
  if (configKey) {
    const recoveredUuid = userUuids[configKey];
    uuidStore[displayName] = recoveredUuid;
    saveUuidStore(uuidStore);
    return recoveredUuid;
  }

  // 3. New user — generate UUID
  const newUuid = uuidv4();
  uuidStore[displayName] = newUuid;
  saveUuidStore(uuidStore);
  userUuids[displayName] = newUuid;
  saveConfig({ userUuids });
  return newUuid;
}

// ============================================================================
// OLD CODE (before fix) — for comparison testing
// ============================================================================

function getUuidForUser_OLD(username) {
  const { v4: uuidv4 } = require('uuid');
  if (!username || !username.trim()) throw new Error('Username required');
  const displayName = username.trim();
  const normalizedLookup = displayName.toLowerCase();

  const config = loadConfig();
  const userUuids = config.userUuids || {};
  const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);

  if (existingKey) {
    return userUuids[existingKey];
  }

  // New user
  const newUuid = uuidv4();
  userUuids[displayName] = newUuid;
  saveConfig({ userUuids });
  return newUuid;
}

function saveConfig_OLD(update) {
  // OLD saveConfig without safety check
  if (!fs.existsSync(TEST_DIR)) fs.mkdirSync(TEST_DIR, { recursive: true });
  const currentConfig = loadConfig();
  // NO SAFETY CHECK — this is the bug
  const newConfig = { ...currentConfig, ...update };
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), 'utf8');
  return true;
}

// ============================================================================
// TESTS
// ============================================================================

console.log('\n' + '='.repeat(70));
console.log('UUID PERSISTENCE TESTS — Simulating update corruption scenarios');
console.log('='.repeat(70));

// --------------------------------------------------------------------------
// TEST 1: Normal flow — UUID stays consistent
// --------------------------------------------------------------------------
console.log('\n--- Test 1: Normal flow — UUID stays consistent ---');
setup();

const uuid1 = getUuidForUser('SpecialK');
const uuid2 = getUuidForUser('SpecialK');
const uuid3 = getUuidForUser('specialk'); // case insensitive

assertEqual(uuid1, uuid2, 'Same username returns same UUID');
assertEqual(uuid1, uuid3, 'Case-insensitive lookup returns same UUID');
assert(uuid1.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i), 'UUID is valid v4 format');

// --------------------------------------------------------------------------
// TEST 2: Simulate update corruption (THE BUG) — old code
// --------------------------------------------------------------------------
console.log('\n--- Test 2: OLD CODE — Config wipe during update loses UUID ---');
setup();

// Setup: player has UUID
const oldConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(oldConfig, null, 2), 'utf8');

const uuidBefore = getUuidForUser_OLD('SpecialK');
assertEqual(uuidBefore, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID correct before corruption');

// Simulate: config.json gets corrupted (loadConfig returns {} because file locked)
// This simulates what happens when saveConfig reads an empty/locked file
fs.writeFileSync(CONFIG_FILE, '', 'utf8'); // Simulate corruption: empty file

// Old saveConfig behavior: reads empty, merges with update, saves
// This wipes userUuids
saveConfig_OLD({ hasLaunchedBefore: true });

const configAfterCorruption = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
assert(!configAfterCorruption.userUuids, 'OLD CODE: userUuids wiped after corruption');
assert(!configAfterCorruption.username, 'OLD CODE: username wiped after corruption');

// Player re-enters name, gets NEW UUID (character data lost!)
const uuidAfterOld = getUuidForUser_OLD('SpecialK');
assert(uuidAfterOld !== uuidBefore, 'OLD CODE: UUID changed after corruption (BUG!)');

// --------------------------------------------------------------------------
// TEST 3: NEW CODE — Config wipe during update, UUID survives via uuid-store
// --------------------------------------------------------------------------
console.log('\n--- Test 3: NEW CODE — Config wipe + UUID survives via uuid-store ---');
setup();

// Setup: player has UUID (stored in both config.json AND uuid-store.json)
const initialConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(initialConfig, null, 2), 'utf8');

// First call migrates to uuid-store
const uuidFirst = getUuidForUser('SpecialK');
assertEqual(uuidFirst, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID correct before corruption');
assert(fs.existsSync(UUID_STORE_FILE), 'uuid-store.json created');

// Simulate: config.json gets wiped (same as the update bug)
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');

// Verify config is empty
const wipedConfig = loadConfig();
assert(!wipedConfig.userUuids || Object.keys(wipedConfig.userUuids).length === 0, 'Config wiped — no userUuids');
assert(!wipedConfig.username, 'Config wiped — no username');

// Player re-enters same name → UUID recovered from uuid-store!
const uuidAfterNew = getUuidForUser('SpecialK');
assertEqual(uuidAfterNew, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'NEW CODE: UUID preserved after config wipe!');

// --------------------------------------------------------------------------
// TEST 4: saveConfig safety check — refuses to overwrite good data with empty
// --------------------------------------------------------------------------
console.log('\n--- Test 4: saveConfig safety check — blocks destructive writes ---');
setup();

// Setup: valid config file with data
const goodConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true, installPath: 'C:\\Games\\Hytale' };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(goodConfig, null, 2), 'utf8');

// Make the file temporarily unreadable by writing garbage (simulates file lock/corruption)
const originalContent = fs.readFileSync(CONFIG_FILE, 'utf8');
fs.writeFileSync(CONFIG_FILE, 'NOT VALID JSON!!!', 'utf8');

// Try to save — should refuse because file exists but can't be parsed
let saveThrew = false;
try {
  saveConfig({ someNewField: true });
} catch (e) {
  saveThrew = true;
}

// The file should still have the garbage (not overwritten with { someNewField: true })
const afterContent = fs.readFileSync(CONFIG_FILE, 'utf8');

// Restore original for backup recovery test
fs.writeFileSync(CONFIG_FILE, JSON.stringify(goodConfig, null, 2), 'utf8');

// Note: with invalid JSON, loadConfig returns {} and safety check triggers
// The save may eventually succeed on retry if the file becomes readable
// What matters is that it doesn't blindly overwrite
assert(afterContent !== '{\n  "someNewField": true\n}', 'Safety check prevented blind overwrite of corrupted file');

// --------------------------------------------------------------------------
// TEST 5: Backup recovery — config.json corrupted, recovered from .bak
// --------------------------------------------------------------------------
console.log('\n--- Test 5: Backup recovery — auto-recover from .bak ---');
setup();

// Create config and backup
const validConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' } };
fs.writeFileSync(CONFIG_BACKUP, JSON.stringify(validConfig, null, 2), 'utf8');
fs.writeFileSync(CONFIG_FILE, 'CORRUPTED', 'utf8');

const recovered = loadConfig();
assertEqual(recovered.username, 'SpecialK', 'Username recovered from backup');
assert(recovered.userUuids && recovered.userUuids['SpecialK'] === 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID recovered from backup');

// --------------------------------------------------------------------------
// TEST 6: Full update simulation — the exact scenario from player report
// --------------------------------------------------------------------------
console.log('\n--- Test 6: Full update simulation (player report scenario) ---');
setup();

// Step 1: Player installs v2.3.4, sets username, plays game
console.log('  Step 1: Player sets up profile...');
saveConfig({ username: 'Special K', hasLaunchedBefore: true });
const originalUuid = getUuidForUser('Special K');
console.log(`  Original UUID: ${originalUuid}`);

// Step 2: v2.3.5 auto-update — new app launches
console.log('  Step 2: Simulating v2.3.5 update...');

// Simulate the 3 saveConfig calls that happen during startup
// But first, simulate config being temporarily locked (returns empty)
const preUpdateContent = fs.readFileSync(CONFIG_FILE, 'utf8');
fs.writeFileSync(CONFIG_FILE, '', 'utf8'); // Simulate: file empty during write (race condition)

// These are the 3 calls from: profileManager.init, migrateUserDataToCentralized, handleFirstLaunchCheck
// With our safety check, they should NOT wipe the data
try { saveConfig({ hasLaunchedBefore: true }); } catch (e) { /* expected — safety check blocks it */ }

// Simulate file becomes readable again (antivirus releases lock)
fs.writeFileSync(CONFIG_FILE, preUpdateContent, 'utf8');

// Step 3: Player re-enters username (because UI might show empty)
console.log('  Step 3: Player re-enters username...');
const postUpdateUuid = getUuidForUser('Special K');
console.log(`  Post-update UUID: ${postUpdateUuid}`);

assertEqual(postUpdateUuid, originalUuid, 'UUID survived the full update cycle!');

// --------------------------------------------------------------------------
// TEST 7: Multiple users — UUIDs stay independent
// --------------------------------------------------------------------------
console.log('\n--- Test 7: Multiple users — UUIDs stay independent ---');
setup();

const uuidAlice = getUuidForUser('Alice');
const uuidBob = getUuidForUser('Bob');
const uuidCharlie = getUuidForUser('Charlie');

assert(uuidAlice !== uuidBob, 'Alice and Bob have different UUIDs');
assert(uuidBob !== uuidCharlie, 'Bob and Charlie have different UUIDs');

// Wipe config, all should survive
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');

assertEqual(getUuidForUser('Alice'), uuidAlice, 'Alice UUID survived config wipe');
assertEqual(getUuidForUser('Bob'), uuidBob, 'Bob UUID survived config wipe');
assertEqual(getUuidForUser('Charlie'), uuidCharlie, 'Charlie UUID survived config wipe');

// --------------------------------------------------------------------------
// TEST 8: UUID store deleted — recovery from config.json
// --------------------------------------------------------------------------
console.log('\n--- Test 8: UUID store deleted — recovery from config.json ---');
setup();

// Create UUID via normal flow (saves to both stores)
const uuidOriginal = getUuidForUser('TestPlayer');

// Delete uuid-store.json (simulates user manually deleting it or disk issue)
fs.unlinkSync(UUID_STORE_FILE);
assert(!fs.existsSync(UUID_STORE_FILE), 'uuid-store.json deleted');

// UUID should be recovered from config.json
const uuidRecovered = getUuidForUser('TestPlayer');
assertEqual(uuidRecovered, uuidOriginal, 'UUID recovered from config.json after uuid-store deletion');
assert(fs.existsSync(UUID_STORE_FILE), 'uuid-store.json recreated after recovery');

// --------------------------------------------------------------------------
// TEST 9: Both stores deleted — new UUID generated (fresh install)
// --------------------------------------------------------------------------
console.log('\n--- Test 9: Both stores deleted — new UUID (fresh install) ---');
setup();

const uuidFresh = getUuidForUser('NewPlayer');

// Delete both
fs.unlinkSync(UUID_STORE_FILE);
fs.unlinkSync(CONFIG_FILE);

const uuidAfterWipe = getUuidForUser('NewPlayer');
assert(uuidAfterWipe !== uuidFresh, 'New UUID generated when both stores are gone (expected for true fresh install)');

// --------------------------------------------------------------------------
// TEST 10: Worst case — config.json wiped AND uuid-store.json exists
// Simulates the EXACT player-reported scenario with new code
// --------------------------------------------------------------------------
console.log('\n--- Test 10: Exact player scenario with new code ---');
setup();

// Player has been playing for a while
saveConfig({
  username: 'Special K',
  hasLaunchedBefore: true,
  installPath: 'C:\\Games\\Hytale',
  version_client: '2026.02.19-1a311a592',
  version_branch: 'release',
  userUuids: { 'Special K': '11111111-2222-4333-9444-555555555555' }
});

// First call creates uuid-store.json
const originalUuid10 = getUuidForUser('Special K');
assertEqual(originalUuid10, '11111111-2222-4333-9444-555555555555', 'Original UUID loaded');

// BOOM: Update happens, config.json completely wiped
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');

// Username lost — player has to re-enter
const loadedUsername = loadConfig().username;
assert(!loadedUsername, 'Username is gone from config (simulating what player saw)');

// Player types "Special K" again in settings
saveConfig({ username: 'Special K' });

// Player clicks Play — getUuidForUser called
const recoveredUuid10 = getUuidForUser('Special K');
assertEqual(recoveredUuid10, '11111111-2222-4333-9444-555555555555', 'UUID recovered — character data preserved!');

// ============================================================================
// RESULTS
// ============================================================================
console.log('\n' + '='.repeat(70));
console.log(`RESULTS: ${passed} passed, ${failed} failed`);
if (failed > 0) {
  console.log('\nFailures:');
  failures.forEach(f => console.log(`  ✗ ${f}`));
}
console.log('='.repeat(70));

cleanup();
process.exit(failed > 0 ? 1 : 0);
