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 { DatabaseSync } from 'node:sqlite';

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

function createFakeSmtpServer() {
  const messages = [];
  const server = net.createServer((socket) => {
    socket.setEncoding('utf8');
    socket.write('220 onlycoin-test-smtp ESMTP\r\n');
    let buffer = '';
    let dataMode = false;
    let message = '';
    socket.on('data', (chunk) => {
      buffer += chunk;
      while (true) {
        if (dataMode) {
          const end = buffer.indexOf('\r\n.\r\n');
          if (end < 0) { message += buffer; buffer = ''; return; }
          message += buffer.slice(0, end);
          buffer = buffer.slice(end + 5);
          messages.push(message);
          message = '';
          dataMode = false;
          socket.write('250 2.0.0 queued\r\n');
          continue;
        }
        const lineEnd = buffer.indexOf('\r\n');
        if (lineEnd < 0) return;
        const line = buffer.slice(0, lineEnd);
        buffer = buffer.slice(lineEnd + 2);
        const command = line.toUpperCase();
        if (command.startsWith('EHLO') || command.startsWith('HELO')) socket.write('250-onlycoin-test-smtp\r\n250 SIZE 10485760\r\n');
        else if (command.startsWith('MAIL FROM:')) socket.write('250 2.1.0 ok\r\n');
        else if (command.startsWith('RCPT TO:')) socket.write('250 2.1.5 ok\r\n');
        else if (command === 'DATA') { dataMode = true; socket.write('354 End data with <CR><LF>.<CR><LF>\r\n'); }
        else if (command === 'RSET') socket.write('250 2.0.0 reset\r\n');
        else if (command === 'QUIT') { socket.write('221 2.0.0 bye\r\n'); socket.end(); }
        else socket.write('250 2.0.0 ok\r\n');
      }
    });
  });
  return { server, messages };
}

function decodeTextParts(rawMessage) {
  const parts = [];
  const regex = /Content-Transfer-Encoding:\s*base64\r?\n\r?\n([A-Za-z0-9+/=\r\n]+)/gi;
  for (const match of rawMessage.matchAll(regex)) {
    try { parts.push(Buffer.from(match[1].replace(/\s+/g, ''), 'base64').toString('utf8')); } catch { /* ignore */ }
  }
  return parts.join('\n');
}

async function waitForHealth(base, child, { installed = null, runtimeReady = null } = {}) {
  for (let attempt = 0; attempt < 160; attempt += 1) {
    if (child.exitCode != null) return null;
    try {
      const response = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(350) });
      if (response.ok) {
        const body = await response.json();
        if ((installed == null || body.installed === installed) && (runtimeReady == null || body.runtimeReady === runtimeReady)) return body;
      }
    } catch { /* retry */ }
    await new Promise((resolve) => setTimeout(resolve, 75));
  }
  return null;
}

async function api(base, pathname, { method = 'GET', body, token = '' } = {}) {
  const headers = { Accept: 'application/json' };
  if (body != null) headers['Content-Type'] = 'application/json';
  if (token) headers['X-Install-Token'] = token;
  const response = await fetch(`${base}${pathname}`, {
    method,
    headers,
    body: body == null ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(15000)
  });
  const payload = await response.json().catch(() => ({}));
  return { status: response.status, ok: response.ok, payload, headers: response.headers };
}

async function stopChild(child) {
  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(); });
  });
}

async function waitForMessage(messages, count) {
  for (let i = 0; i < 100; i += 1) {
    if (messages.length >= count) return;
    await new Promise((resolve) => setTimeout(resolve, 50));
  }
  throw new Error(`SMTP mesajı beklenirken zaman aşımı: ${messages.length}/${count}`);
}

test('four-step installer creates a zero-data production runtime and verified registration', async (t) => {
  const rootDir = path.resolve(import.meta.dirname, '../..');
  const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'onlycoin-installer-'));
  const dataDir = path.join(workspace, 'data');
  const configDir = path.join(workspace, 'config');
  const envPath = path.join(configDir, '.env');
  fs.mkdirSync(configDir, { recursive: true });
  const port = await reservePort();
  const smtpPort = await reservePort();
  const base = `http://127.0.0.1:${port}`;
  const smtp = createFakeSmtpServer();
  await new Promise((resolve, reject) => smtp.server.listen(smtpPort, '127.0.0.1', (error) => error ? reject(error) : resolve()));

  const outputs = [];
  const baseEnv = { ...process.env, ONLYCOIN_ENV_PATH: envPath, DATA_DIR: dataDir, HOST: '127.0.0.1', PORT: String(port), APP_ORIGIN: base };
  delete baseEnv.NODE_ENV;
  const first = spawn(process.execPath, ['--no-warnings', 'server/src/main.js'], {
    cwd: rootDir,
    env: { ...baseEnv, NODE_ENV: 'installation' },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true
  });
  first.stdout.on('data', (chunk) => outputs.push(chunk.toString('utf8')));
  first.stderr.on('data', (chunk) => outputs.push(chunk.toString('utf8')));

  let second;
  t.after(async () => {
    await stopChild(first);
    if (second) await stopChild(second);
    await new Promise((resolve) => smtp.server.close(resolve));
    fs.rmSync(workspace, { recursive: true, force: true });
  });

  const initialHealth = await waitForHealth(base, first, { installed: false, runtimeReady: false });
  assert.ok(initialHealth, outputs.join(''));

  const installerHtml = await fetch(`${base}/install/`).then((response) => response.text());
  const installerJs = await fetch(`${base}/install/app.js`).then((response) => response.text());
  assert.match(installerHtml, /OnlyCoin Kurulum Sihirbazı/);
  for (const step of ['AŞAMA 1 / 4', 'AŞAMA 2 / 4', 'AŞAMA 3 / 4', 'AŞAMA 4 / 4']) assert.match(installerJs, new RegExp(step.replace('/', '\\/')));

  const tokenPath = path.join(dataDir, 'install.token');
  assert.ok(fs.existsSync(tokenPath));
  const installToken = fs.readFileSync(tokenPath, 'utf8').trim();
  assert.ok(installToken.length >= 24);

  const protectedApi = await api(base, '/api/public/config');
  assert.equal(protectedApi.status, 503);
  assert.equal(protectedApi.payload.code, 'INSTALL_REQUIRED');

  const adminEmail = 'owner@onlycoin.test';
  const adminPassword = 'OnlyCoin-Live-Owner!2026';
  const smtpConfig = {
    host: '127.0.0.1', port: smtpPort, secure: false, startTls: false,
    username: '', password: '', fromEmail: 'noreply@onlycoin.test', fromName: 'OnlyCoin', ehloName: 'onlycoin.test'
  };
  const smtpTest = await api(base, '/api/install/test-smtp', {
    method: 'POST', token: installToken,
    body: { smtp: smtpConfig, recipient: adminEmail }
  });
  assert.equal(smtpTest.status, 200, JSON.stringify(smtpTest.payload));
  await waitForMessage(smtp.messages, 1);

  const completed = await api(base, '/api/install/complete', {
    method: 'POST', token: installToken,
    body: {
      site: { appOrigin: base, appName: 'OnlyCoin', companyName: 'OnlyCoin Teknoloji', supportEmail: adminEmail, timezone: 'Europe/Istanbul' },
      admin: { email: adminEmail, name: 'OnlyCoin Owner', password: adminPassword, confirmPassword: adminPassword },
      smtp: smtpConfig,
      smtpTestRecipient: adminEmail
    }
  });
  assert.equal(completed.status, 201, JSON.stringify(completed.payload));
  assert.equal(completed.payload.restartRequired, true);
  await waitForMessage(smtp.messages, 2);
  const firstExit = await new Promise((resolve) => first.once('exit', resolve));
  assert.equal(firstExit, 75, outputs.join(''));

  const restartEnv = { ...baseEnv };
  delete restartEnv.NODE_ENV;
  second = spawn(process.execPath, ['--no-warnings', 'server/src/main.js'], {
    cwd: rootDir,
    env: restartEnv,
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true
  });
  second.stdout.on('data', (chunk) => outputs.push(chunk.toString('utf8')));
  second.stderr.on('data', (chunk) => outputs.push(chunk.toString('utf8')));
  const liveHealth = await waitForHealth(base, second, { installed: true, runtimeReady: true });
  assert.equal(liveHealth?.environment, 'production', outputs.join(''));

  const dbPath = path.join(dataDir, 'onlycoin.db');
  const db = new DatabaseSync(dbPath, { readOnly: true });
  assert.equal(Number(db.prepare('SELECT COUNT(*) AS count FROM users').get().count), 0);
  assert.equal(Number(db.prepare('SELECT COUNT(*) AS count FROM ledger_transactions').get().count), 0);
  assert.equal(Number(db.prepare('SELECT COUNT(*) AS count FROM payment_orders').get().count), 0);
  assert.equal(Number(db.prepare('SELECT COUNT(*) AS count FROM deposits').get().count), 0);
  assert.equal(Number(db.prepare('SELECT COUNT(*) AS count FROM withdrawals').get().count), 0);
  assert.equal(Number(db.prepare("SELECT json_extract(value,'$') AS value FROM settings WHERE key='initial_points'").get().value), 0);
  assert.equal(Number(db.prepare("SELECT json_extract(value,'$') AS value FROM settings WHERE key='conversion_enabled'").get().value), 0);
  db.close();

  const adminLogin = await api(base, '/api/admin/login', { method: 'POST', body: { email: adminEmail, password: adminPassword } });
  assert.equal(adminLogin.status, 200, JSON.stringify(adminLogin.payload));
  assert.ok(adminLogin.payload.admin?.id);

  const userEmail = 'new.member@onlycoin.test';
  const register = await api(base, '/api/auth/register', {
    method: 'POST',
    body: { email: userEmail, username: 'NewMember', displayName: 'New Member', password: 'OnlyCoin-New-Member!2026', confirmPassword: 'OnlyCoin-New-Member!2026' }
  });
  assert.equal(register.status, 201, JSON.stringify(register.payload));
  assert.equal(register.payload.verificationRequired, true);
  assert.equal(register.payload.emailDelivery?.delivered, true);
  assert.equal(Object.hasOwn(register.payload, 'debugVerificationCode'), false);
  await waitForMessage(smtp.messages, 3);
  const verificationMail = decodeTextParts(smtp.messages.at(-1));
  const code = verificationMail.match(/\b(\d{6})\b/)?.[1];
  assert.match(code || '', /^\d{6}$/, verificationMail);

  const verify = await api(base, '/api/auth/email/verify', { method: 'POST', body: { email: userEmail, code } });
  assert.equal(verify.status, 200, JSON.stringify(verify.payload));
  assert.ok(verify.payload.user?.emailVerifiedAt);
  const login = await api(base, '/api/auth/email/login', { method: 'POST', body: { email: userEmail, password: 'OnlyCoin-New-Member!2026' } });
  assert.equal(login.status, 200, JSON.stringify(login.payload));
  assert.equal(login.payload.user.email, userEmail);

  const postDb = new DatabaseSync(dbPath, { readOnly: true });
  assert.equal(Number(postDb.prepare('SELECT COUNT(*) AS count FROM users').get().count), 1);
  const memberId = postDb.prepare('SELECT id FROM users WHERE email=?').get(userEmail).id;
  const points = Number(postDb.prepare("SELECT balance FROM ledger_accounts WHERE user_id=? AND asset='OC_POINTS'").get(memberId).balance);
  assert.equal(points, 0);
  assert.equal(Number(postDb.prepare('SELECT COUNT(*) AS count FROM ledger_transactions').get().count), 0);
  postDb.close();
});
