/**
 * Smoke tests: `mcpctl passwd` end-to-end against live mcpd.
 *
 * Exercises the full password-change contract:
 *   1. Admin creates a throwaway user (gets the default self-permission seeded).
 *   2. That user self-changes their password (POST /users/me/password) with the
 *      correct current password → succeeds; new password logs in, old does not.
 *   3. Wrong current password → 401, password unchanged.
 *   4. Admin resets the user's password (PUT /users/:id/password) → new one logs in.
 *   5. Cleanup: delete the throwaway user.
 *
 * Target: $MCPD_URL (default https://mcpctl.ad.itaz.eu). Admin token is read
 * from the local mcpctl credentials file (the box running smoke tests is logged
 * in as admin). If /healthz fails or no admin token is found, the suite skips.
 *
 * Run with: pnpm test:smoke
 */
import { describe, it, expect, beforeAll } from 'vitest';
import http from 'node:http';
import https from 'node:https';
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

const MCPD_URL = process.env.MCPD_URL ?? 'https://mcpctl.ad.itaz.eu';
const STAMP = Date.now().toString(36);
const EMAIL = `smoke-passwd-${STAMP}@example.com`;
const P1 = `Smoke-${STAMP}-aaa1`;
const P2 = `Smoke-${STAMP}-bbb2`;
const P3 = `Smoke-${STAMP}-ccc3`;

interface Resp { status: number; body: string; json: <T = unknown>() => T }

function request(method: string, url: string, token?: string, payload?: unknown): Promise<Resp> {
  return new Promise((resolve, reject) => {
    const u = new URL(url);
    const driver = u.protocol === 'https:' ? https : http;
    const data = payload === undefined ? undefined : JSON.stringify(payload);
    const headers: Record<string, string> = {};
    if (token) headers['Authorization'] = `Bearer ${token}`;
    if (data) { headers['Content-Type'] = 'application/json'; headers['Content-Length'] = String(Buffer.byteLength(data)); }
    const req = driver.request(url, { method, headers, timeout: 15_000 }, (res) => {
      const chunks: Buffer[] = [];
      res.on('data', (c: Buffer) => chunks.push(c));
      res.on('end', () => {
        const body = Buffer.concat(chunks).toString('utf-8');
        resolve({ status: res.statusCode ?? 0, body, json: <T,>() => JSON.parse(body) as T });
      });
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
    if (data) req.write(data);
    req.end();
  });
}

async function healthz(): Promise<boolean> {
  try {
    const res = await request('GET', `${MCPD_URL.replace(/\/$/, '')}/healthz`);
    return res.status === 200;
  } catch { return false; }
}

function adminToken(): string | undefined {
  // The CLI stores credentials at ~/.mcpctl/credentials (see config/loader.ts).
  for (const p of [join(homedir(), '.mcpctl', 'credentials'), join(homedir(), '.config', 'mcpctl', 'credentials')]) {
    try {
      const tok = (JSON.parse(readFileSync(p, 'utf-8')) as { token?: string }).token;
      if (tok) return tok;
    } catch { /* try next */ }
  }
  return undefined;
}

async function login(email: string, password: string): Promise<Resp> {
  return request('POST', `${MCPD_URL}/api/v1/auth/login`, undefined, { email, password });
}

describe('passwd smoke', () => {
  let token: string | undefined;
  let ready = false;
  let userId: string | undefined;

  beforeAll(async () => {
    token = adminToken();
    const up = await healthz();
    ready = up && token !== undefined;
    if (!ready) {
      console.warn(`\n  ○ passwd smoke: skipped — ${MCPD_URL}/healthz up=${up}, adminToken=${token !== undefined}.\n`);
      return;
    }
    // Create the throwaway user (admin).
    const res = await request('POST', `${MCPD_URL}/api/v1/users`, token, { email: EMAIL, password: P1 });
    expect([201, 409]).toContain(res.status);
    const me = await request('GET', `${MCPD_URL}/api/v1/users/${encodeURIComponent(EMAIL)}`, token);
    userId = me.json<{ id: string }>().id;
  });

  it('self-change succeeds with correct current password; new logs in, old does not', async () => {
    if (!ready) return;
    const userTok = (await login(EMAIL, P1)).json<{ token: string }>().token;
    const chg = await request('POST', `${MCPD_URL}/api/v1/users/me/password`, userTok, { currentPassword: P1, newPassword: P2 });
    expect(chg.status).toBe(200);
    expect((await login(EMAIL, P2)).status).toBe(200);
    expect((await login(EMAIL, P1)).status).toBe(401);
  });

  it('self-change with wrong current password is rejected (401)', async () => {
    if (!ready) return;
    const userTok = (await login(EMAIL, P2)).json<{ token: string }>().token;
    const chg = await request('POST', `${MCPD_URL}/api/v1/users/me/password`, userTok, { currentPassword: 'definitely-wrong', newPassword: P3 });
    expect(chg.status).toBe(401);
    expect((await login(EMAIL, P2)).status).toBe(200); // unchanged
  });

  it('admin reset sets a new password without the current one', async () => {
    if (!ready || !userId) return;
    const reset = await request('PUT', `${MCPD_URL}/api/v1/users/${userId}/password`, token, { newPassword: P3 });
    expect(reset.status).toBe(200);
    expect((await login(EMAIL, P3)).status).toBe(200);
  });

  it('cleanup: delete the throwaway user', async () => {
    if (!ready || !userId) return;
    const del = await request('DELETE', `${MCPD_URL}/api/v1/users/${userId}`, token);
    expect([204, 404]).toContain(del.status);
  });
});
