/**
 * Smoke tests: Personality + agent-direct prompts against a live mcpd.
 *
 * Validates Stages 1-4 end-to-end without needing a live LLM upstream:
 *   1. Create the supporting Secret + Llm + Agent (mcpctl CLI).
 *   2. Create a Personality on the agent (POST /api/v1/agents/:name/personalities).
 *   3. Create an agent-direct prompt (POST /api/v1/prompts with `agent: name`).
 *   4. Attach the prompt; verify the binding shows up.
 *   5. Reject double-attach (409) and out-of-scope attach (400).
 *   6. PUT the agent's defaultPersonality by name.
 *   7. Cleanup: detach, delete personality, delete agent, delete llm/secret.
 *
 * The chat-time overlay path is covered by the new mcpd unit tests
 * (chat-service.test.ts); a future agent-chat smoke run with the right
 * env vars exercises it through the full SSE pipe.
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import http from 'node:http';
import https from 'node:https';
import { execSync } from 'node:child_process';

const MCPD_URL = process.env.MCPD_URL ?? 'https://mcpctl.ad.itaz.eu';
const SUFFIX = Date.now().toString(36);
const SECRET_NAME = `smoke-pers-sec-${SUFFIX}`;
const LLM_NAME = `smoke-pers-llm-${SUFFIX}`;
const AGENT_NAME = `smoke-pers-agent-${SUFFIX}`;
const PERSONALITY_NAME = `smoke-pers-${SUFFIX}`;
const DIRECT_PROMPT_NAME = `smoke-pers-direct-${SUFFIX}`;

interface CliResult { code: number; stdout: string; stderr: string }

function run(args: string): CliResult {
  try {
    const stdout = execSync(`mcpctl --direct ${args}`, {
      encoding: 'utf-8',
      timeout: 30_000,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    return { code: 0, stdout: stdout.trim(), stderr: '' };
  } catch (err) {
    const e = err as { status?: number; stdout?: Buffer | string; stderr?: Buffer | string };
    return {
      code: e.status ?? 1,
      stdout: e.stdout ? (typeof e.stdout === 'string' ? e.stdout : e.stdout.toString('utf-8')) : '',
      stderr: e.stderr ? (typeof e.stderr === 'string' ? e.stderr : e.stderr.toString('utf-8')) : '',
    };
  }
}

function healthz(url: string, timeoutMs = 5000): Promise<boolean> {
  return new Promise((resolve) => {
    const parsed = new URL(`${url.replace(/\/$/, '')}/healthz`);
    const driver = parsed.protocol === 'https:' ? https : http;
    const req = driver.get(
      {
        hostname: parsed.hostname,
        port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
        path: parsed.pathname,
        timeout: timeoutMs,
      },
      (res) => { resolve((res.statusCode ?? 500) < 500); res.resume(); },
    );
    req.on('error', () => resolve(false));
    req.on('timeout', () => { req.destroy(); resolve(false); });
  });
}

let mcpdUp = false;
let createdPromptId: string | null = null;
let createdPersonalityId: string | null = null;

describe('personality smoke', () => {
  beforeAll(async () => {
    mcpdUp = await healthz(MCPD_URL);
    if (!mcpdUp) {
      // eslint-disable-next-line no-console
      console.warn(`\n  ○ personality smoke: skipped — ${MCPD_URL}/healthz unreachable.\n`);
    }
  }, 20_000);

  afterAll(async () => {
    if (!mcpdUp) return;
    if (createdPersonalityId !== null) {
      await httpRequest('DELETE', `${MCPD_URL}/api/v1/personalities/${createdPersonalityId}`, undefined);
    }
    if (createdPromptId !== null) {
      await httpRequest('DELETE', `${MCPD_URL}/api/v1/prompts/${createdPromptId}`, undefined);
    }
    run(`delete agent ${AGENT_NAME}`);
    run(`delete llm ${LLM_NAME}`);
    run(`delete secret ${SECRET_NAME}`);
  });

  it('seeds Secret + Llm + Agent', () => {
    if (!mcpdUp) return;
    run(`delete secret ${SECRET_NAME}`);
    run(`delete llm ${LLM_NAME}`);
    run(`delete agent ${AGENT_NAME}`);

    expect(run(`create secret ${SECRET_NAME} --data API_KEY=sk-fake`).code).toBe(0);
    expect(run([
      `create llm ${LLM_NAME}`,
      '--type openai',
      '--model gpt-4o-mini',
      '--url http://localhost:9999',
      `--api-key-ref ${SECRET_NAME}/API_KEY`,
    ].join(' ')).code).toBe(0);
    expect(run([
      `create agent ${AGENT_NAME}`,
      `--llm ${LLM_NAME}`,
      `--description "smoke personality agent"`,
    ].join(' ')).code).toBe(0);
  });

  it('creates an agent-direct prompt', async () => {
    if (!mcpdUp) return;
    const res = await httpRequest('POST', `${MCPD_URL}/api/v1/prompts`, {
      name: DIRECT_PROMPT_NAME,
      content: 'Always be terse.',
      agent: AGENT_NAME,
      priority: 8,
    });
    expect(res.status).toBe(201);
    const body = JSON.parse(res.body) as { id: string; agentId: string };
    expect(body.agentId).toBeTruthy();
    createdPromptId = body.id;
  });

  it('lists agent-direct prompts via GET /api/v1/agents/:name/prompts', async () => {
    if (!mcpdUp) return;
    const res = await httpRequest('GET', `${MCPD_URL}/api/v1/agents/${AGENT_NAME}/prompts`, undefined);
    expect(res.status).toBe(200);
    const rows = JSON.parse(res.body) as Array<{ name: string }>;
    expect(rows.some((r) => r.name === DIRECT_PROMPT_NAME)).toBe(true);
  });

  it('creates a personality on the agent', async () => {
    if (!mcpdUp) return;
    const res = await httpRequest(
      'POST',
      `${MCPD_URL}/api/v1/agents/${AGENT_NAME}/personalities`,
      {
        name: PERSONALITY_NAME,
        description: 'smoke personality',
        priority: 7,
      },
    );
    expect(res.status, res.body).toBe(201);
    const body = JSON.parse(res.body) as { id: string; name: string; promptCount: number };
    expect(body.name).toBe(PERSONALITY_NAME);
    expect(body.promptCount).toBe(0);
    createdPersonalityId = body.id;
  });

  it('rejects duplicate personality name on the same agent (409)', async () => {
    if (!mcpdUp) return;
    const res = await httpRequest(
      'POST',
      `${MCPD_URL}/api/v1/agents/${AGENT_NAME}/personalities`,
      { name: PERSONALITY_NAME },
    );
    expect(res.status).toBe(409);
  });

  it('lists the personality via /api/v1/personalities and the per-agent route', async () => {
    if (!mcpdUp) return;
    const all = await httpRequest('GET', `${MCPD_URL}/api/v1/personalities`, undefined);
    expect(all.status).toBe(200);
    const allRows = JSON.parse(all.body) as Array<{ name: string; agentName: string }>;
    expect(allRows.some((r) => r.name === PERSONALITY_NAME && r.agentName === AGENT_NAME)).toBe(true);

    const perAgent = await httpRequest(
      'GET',
      `${MCPD_URL}/api/v1/agents/${AGENT_NAME}/personalities`,
      undefined,
    );
    expect(perAgent.status).toBe(200);
    const perAgentRows = JSON.parse(perAgent.body) as Array<{ name: string }>;
    expect(perAgentRows.map((r) => r.name)).toContain(PERSONALITY_NAME);
  });

  it('attaches the agent-direct prompt and lists the binding', async () => {
    if (!mcpdUp || createdPersonalityId === null || createdPromptId === null) return;
    const attach = await httpRequest(
      'POST',
      `${MCPD_URL}/api/v1/personalities/${createdPersonalityId}/prompts`,
      { promptId: createdPromptId, priority: 9 },
    );
    expect(attach.status, attach.body).toBe(201);

    const list = await httpRequest(
      'GET',
      `${MCPD_URL}/api/v1/personalities/${createdPersonalityId}/prompts`,
      undefined,
    );
    expect(list.status).toBe(200);
    const rows = JSON.parse(list.body) as Array<{ promptName: string; priority: number }>;
    const found = rows.find((r) => r.promptName === DIRECT_PROMPT_NAME);
    expect(found, `binding for ${DIRECT_PROMPT_NAME} must be present`).toBeDefined();
    expect(found!.priority).toBe(9);
  });

  it('rejects double-attach of the same prompt (409)', async () => {
    if (!mcpdUp || createdPersonalityId === null || createdPromptId === null) return;
    const res = await httpRequest(
      'POST',
      `${MCPD_URL}/api/v1/personalities/${createdPersonalityId}/prompts`,
      { promptId: createdPromptId },
    );
    expect(res.status).toBe(409);
  });

  it('rejects attaching a prompt belonging to a different agent (400)', async () => {
    if (!mcpdUp || createdPersonalityId === null) return;
    // Spawn a second agent + a prompt direct on it; foreign attach must 400.
    const otherAgent = `smoke-pers-other-${SUFFIX}`;
    expect(run([
      `create agent ${otherAgent}`,
      `--llm ${LLM_NAME}`,
    ].join(' ')).code).toBe(0);
    const foreignPrompt = await httpRequest('POST', `${MCPD_URL}/api/v1/prompts`, {
      name: `smoke-pers-foreign-${SUFFIX}`,
      content: 'foreign',
      agent: otherAgent,
    });
    expect(foreignPrompt.status).toBe(201);
    const foreignId = (JSON.parse(foreignPrompt.body) as { id: string }).id;

    try {
      const res = await httpRequest(
        'POST',
        `${MCPD_URL}/api/v1/personalities/${createdPersonalityId}/prompts`,
        { promptId: foreignId },
      );
      expect(res.status).toBe(400);
    } finally {
      await httpRequest('DELETE', `${MCPD_URL}/api/v1/prompts/${foreignId}`, undefined);
      run(`delete agent ${otherAgent}`);
    }
  });

  it('sets defaultPersonality on the agent by name', async () => {
    if (!mcpdUp) return;
    // Resolve agent id for PUT.
    const res = await httpRequest('GET', `${MCPD_URL}/api/v1/agents/${AGENT_NAME}`, undefined);
    expect(res.status).toBe(200);
    const agent = JSON.parse(res.body) as { id: string };

    const put = await httpRequest('PUT', `${MCPD_URL}/api/v1/agents/${agent.id}`, {
      defaultPersonality: { name: PERSONALITY_NAME },
    });
    expect(put.status, put.body).toBe(200);
    const updated = JSON.parse(put.body) as { defaultPersonality: { name: string } | null };
    expect(updated.defaultPersonality?.name).toBe(PERSONALITY_NAME);
  });

  it('detaches the prompt and deletes the personality', async () => {
    if (!mcpdUp || createdPersonalityId === null || createdPromptId === null) return;
    const detach = await httpRequest(
      'DELETE',
      `${MCPD_URL}/api/v1/personalities/${createdPersonalityId}/prompts/${createdPromptId}`,
      undefined,
    );
    expect(detach.status).toBe(204);

    const del = await httpRequest(
      'DELETE',
      `${MCPD_URL}/api/v1/personalities/${createdPersonalityId}`,
      undefined,
    );
    expect(del.status).toBe(204);
    createdPersonalityId = null;
  });
});

interface HttpResponse { status: number; body: string }

function httpRequest(method: string, urlStr: string, body: unknown): Promise<HttpResponse> {
  return new Promise((resolve, reject) => {
    const tokenRaw = readToken();
    const parsed = new URL(urlStr);
    const driver = parsed.protocol === 'https:' ? https : http;
    const headers: Record<string, string> = {
      Accept: 'application/json',
      ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
      ...(tokenRaw !== null ? { Authorization: `Bearer ${tokenRaw}` } : {}),
    };
    const req = driver.request({
      hostname: parsed.hostname,
      port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
      path: parsed.pathname + parsed.search,
      method,
      headers,
      timeout: 15_000,
    }, (res) => {
      const chunks: Buffer[] = [];
      res.on('data', (c: Buffer) => chunks.push(c));
      res.on('end', () => {
        resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf-8') });
      });
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error(`httpRequest timeout: ${method} ${urlStr}`)); });
    if (body !== undefined) req.write(JSON.stringify(body));
    req.end();
  });
}

function readToken(): string | null {
  try {
    const home = process.env.HOME ?? '';
    const path = `${home}/.mcpctl/credentials`;
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const fs = require('node:fs') as typeof import('node:fs');
    if (!fs.existsSync(path)) return null;
    const raw = fs.readFileSync(path, 'utf-8');
    const parsed = JSON.parse(raw) as { token?: string };
    return parsed.token ?? null;
  } catch {
    return null;
  }
}
