import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { prepareTestRuntime, registerVerifiedUser, serverTestEnv, TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD } from './helpers.js';

function reservePort() {
  return new Promise((resolve, reject) => {
    const server = net.createServer();
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      const address = server.address();
      const port = typeof address === 'object' && address ? address.port : 0;
      server.close((error) => error ? reject(error) : resolve(port));
    });
  });
}

async function rawRequest(base, pathname, { method = 'GET', token = '', body, idempotencyKey = '' } = {}) {
  const headers = { Accept: 'application/json', 'X-Request-ID': crypto.randomUUID() };
  if (token) headers.Authorization = `Bearer ${token}`;
  if (body != null) headers['Content-Type'] = 'application/json';
  if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
  const response = await fetch(`${base}${pathname}`, {
    method,
    headers,
    body: body == null ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(10000)
  });
  const contentType = response.headers.get('content-type') || '';
  const payload = contentType.includes('application/json')
    ? await response.json().catch(() => ({}))
    : await response.text();
  return { status: response.status, ok: response.ok, payload };
}

async function request(base, pathname, options = {}) {
  const result = await rawRequest(base, pathname, options);
  if (!result.ok) {
    const payload = typeof result.payload === 'object' ? result.payload : {};
    throw Object.assign(new Error(payload.message || `${options.method || 'GET'} ${pathname} failed with ${result.status}`), {
      status: result.status,
      code: payload.code,
      payload
    });
  }
  return result.payload;
}

async function waitForHealth(base, child, output) {
  for (let attempt = 0; attempt < 120; attempt += 1) {
    if (child.exitCode != null) return false;
    try {
      const response = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(300) });
      if (response.ok) return true;
    } catch {}
    await new Promise((resolve) => setTimeout(resolve, 75));
  }
  throw new Error(`OnlyCoin wheel/CMS test server did not become healthy.\n${output.join('')}`);
}

const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));

async function balance(base, token) {
  return (await request(base, '/api/game/state', { token })).balances.OC_POINTS;
}

test('wheel is atomic, rapid-click safe, admin-managed and CMS pages are editable', async (t) => {
  const port = await reservePort();
  const base = `http://127.0.0.1:${port}`;
  const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onlycoin-wheel-cms-http-'));
  const output = [];
  const rootDir = path.resolve(import.meta.dirname, '../..');
  prepareTestRuntime(rootDir, dataDir, { APP_ORIGIN: base });
  const child = spawn(process.execPath, ['--no-warnings', 'server/src/main.js'], {
    cwd: rootDir,
    env: serverTestEnv({ dataDir, port, base }),
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true
  });
  child.stdout.on('data', (chunk) => output.push(chunk.toString('utf8')));
  child.stderr.on('data', (chunk) => output.push(chunk.toString('utf8')));

  t.after(async () => {
    if (child.exitCode == null) child.kill('SIGTERM');
    await new Promise((resolve) => {
      if (child.exitCode != null) return resolve();
      const timer = setTimeout(() => {
        if (child.exitCode == null) child.kill('SIGKILL');
        resolve();
      }, 3000);
      child.once('exit', () => { clearTimeout(timer); resolve(); });
    });
    fs.rmSync(dataDir, { recursive: true, force: true });
  });

  await waitForHealth(base, child, output);

  const admin = await request(base, '/api/admin/login', {
    method: 'POST',
    body: { email: TEST_ADMIN_EMAIL, password: TEST_ADMIN_PASSWORD }
  });
  const adminToken = admin.token;

  await request(base, '/api/admin/settings', {
    method: 'PUT', token: adminToken,
    body: {
      wheel_daily_free_spins: 3,
      wheel_daily_total_limit: 10,
      wheel_spin_cooldown_seconds: 1,
      wheel_animation_ms: 1800,
      wheel_prevent_immediate_repeat: true
    }
  });

  const segments = await request(base, '/api/admin/wheel', { token: adminToken });
  for (const segment of segments) {
    await request(base, `/api/admin/wheel/${encodeURIComponent(segment.id)}`, {
      method: 'PUT', token: adminToken,
      body: {
        enabled: segment.id === 'points-750',
        weight: segment.id === 'points-750' ? 1 : 0,
        color: segment.id === 'points-750' ? '#28180a' : segment.color
      }
    });
  }

  const { verified: user } = await registerVerifiedUser(request, base, {
    email: 'wheel-race-user@example.com',
    username: 'WheelRaceUser',
    displayName: 'Wheel Race User'
  });
  const userToken = user.token;
  const before = await balance(base, userToken);

  const concurrentKeys = Array.from({ length: 12 }, (_, index) => `wheel-race-${String(index).padStart(2, '0')}-key`);
  const results = await Promise.all(concurrentKeys.map((key) => rawRequest(base, '/api/game/wheel/spin', {
    method: 'POST', token: userToken, idempotencyKey: key, body: { mode: 'free' }
  })));
  const successes = results.filter((result) => result.status === 200);
  const rejected = results.filter((result) => result.status !== 200);
  assert.equal(successes.length, 1, JSON.stringify(results));
  assert.ok(rejected.every((result) => result.status === 429 && result.payload.code === 'SPIN_IN_PROGRESS'), JSON.stringify(rejected));
  assert.equal(await balance(base, userToken), before + 750, 'rapid clicks must credit exactly one reward');

  const successfulKey = concurrentKeys[results.findIndex((result) => result.status === 200)];
  const duplicate = await request(base, '/api/game/wheel/spin', {
    method: 'POST', token: userToken, idempotencyKey: successfulKey, body: { mode: 'free' }
  });
  assert.equal(duplicate.duplicate, true);
  assert.equal(await balance(base, userToken), before + 750, 'idempotent replay must not credit again');

  // The effective server lock is at least the animation duration (1.8s -> 2s).
  await sleep(2100);
  await request(base, '/api/game/wheel/spin', {
    method: 'POST', token: userToken, idempotencyKey: 'wheel-second-free-key', body: { mode: 'free' }
  });
  await sleep(2100);
  await request(base, '/api/game/wheel/spin', {
    method: 'POST', token: userToken, idempotencyKey: 'wheel-third-free-key', body: { mode: 'free' }
  });
  await sleep(2100);
  const fourth = await rawRequest(base, '/api/game/wheel/spin', {
    method: 'POST', token: userToken, idempotencyKey: 'wheel-fourth-free-key', body: { mode: 'free' }
  });
  assert.equal(fourth.status, 409);
  assert.equal(fourth.payload.code, 'FREE_SPIN_USED');
  assert.equal(await balance(base, userToken), before + 2250, 'three configured spins must produce three rewards only');

  const wheelAfter = await request(base, '/api/game/wheel', { token: userToken });
  assert.equal(wheelAfter.state.state.wheel.freeSpinsAllowed, 3);
  assert.equal(wheelAfter.state.state.wheel.freeSpinsRemaining, 0);
  assert.equal(wheelAfter.state.state.wheel.totalSpinsUsed, 3);

  // Admin can change the entitlement without touching code or the database manually.
  await request(base, '/api/admin/settings', {
    method: 'PUT', token: adminToken, body: { wheel_daily_free_spins: 5 }
  });
  const wheelManaged = await request(base, '/api/game/wheel', { token: userToken });
  assert.equal(wheelManaged.state.state.wheel.freeSpinsAllowed, 5);
  assert.equal(wheelManaged.state.state.wheel.freeSpinsRemaining, 2);

  const adminSite = await request(base, '/api/admin/site', { token: adminToken });
  assert.ok(adminSite.pages.some((row) => row.slug === 'tokenomics'));
  assert.ok(adminSite.pages.some((row) => row.slug === 'roadmap'));
  assert.ok(adminSite.pages.some((row) => row.slug === 'whitepaper'));

  await request(base, '/api/admin/site/pages/tokenomics', {
    method: 'PUT', token: adminToken,
    body: { hero_title: 'Test Tokenomics Başlığı', hero_accent: 'Yönetilebilir', enabled: true }
  });
  const allocation = adminSite.allocations[0];
  await request(base, `/api/admin/tokenomics/allocations/${encodeURIComponent(allocation.id)}`, {
    method: 'PUT', token: adminToken,
    body: { label: 'Test Topluluk Havuzu', percentage_bps: allocation.percentage_bps, token_amount: allocation.token_amount, enabled: true }
  });
  const phase = adminSite.roadmap[0];
  await request(base, `/api/admin/roadmap/${encodeURIComponent(phase.id)}`, {
    method: 'PUT', token: adminToken,
    body: { title: 'Test Foundation Phase', items: ['Test madde 1', 'Test madde 2'], enabled: true }
  });
  const chapter = adminSite.chapters[0];
  await request(base, `/api/admin/whitepaper/chapters/${encodeURIComponent(chapter.id)}`, {
    method: 'PUT', token: adminToken,
    body: { title: 'Test Whitepaper Giriş', summary: 'CMS testi', content_html: '<p>Test içerik</p>', enabled: true }
  });

  const publicSite = await request(base, '/api/public/site');
  assert.equal(publicSite.tokenomics.totalSupply, 10_000_000_000, 'managed total supply must match the 10B allocation model');
  assert.equal(publicSite.pages.find((row) => row.slug === 'tokenomics').hero_title, 'Test Tokenomics Başlığı');
  assert.equal(publicSite.tokenomics.allocations.find((row) => row.id === allocation.id).label, 'Test Topluluk Havuzu');
  assert.equal(publicSite.roadmap.find((row) => row.id === phase.id).title, 'Test Foundation Phase');
  assert.equal(publicSite.whitepaper.chapters.find((row) => row.id === chapter.id).title, 'Test Whitepaper Giriş');

  for (const route of ['/', '/presale', '/tokenomics', '/roadmap', '/whitepaper', '/game/', '/admin/']) {
    const response = await fetch(`${base}${route}`, { signal: AbortSignal.timeout(5000) });
    assert.equal(response.status, 200, `${route} must return 200`);
    const html = await response.text();
    assert.match(html, /OnlyCoin|ONLYCOIN/i, `${route} should render OnlyCoin HTML`);
  }
});
