import { randomUUID } from 'node:crypto';
import fs from 'node:fs/promises';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ServerDefinition } from '../src/config.js';
import {
  __testProcessRequest,
  cleanupDaemonArtifactsIfOwned,
  isDaemonResponding,
  metadataMatches,
} from '../src/daemon/host.js';
import type { DaemonRequest } from '../src/daemon/protocol.js';
import type { Runtime } from '../src/runtime.js';

describe('daemon host request handling', () => {
  const metadata = {
    configPath: '/tmp/config.json',
    configLayers: [],
    configMtimeMs: Date.now(),
    socketPath: '/tmp/socket',
    startedAt: Date.now(),
    logPath: null,
  };
  const logContext = { enabled: false, logAllServers: false, servers: new Set<string>() };

  it('reuses pre-parsed requests without reparsing payloads', async () => {
    const parsedRequest: DaemonRequest = { id: '1', method: 'status', params: {} };
    const result = await __testProcessRequest(
      '!!!invalid-json!!!',
      {} as Runtime,
      new Map<string, ServerDefinition>(),
      new Map(),
      metadata,
      logContext,
      parsedRequest
    );

    expect(result.response.ok).toBe(true);
    expect(result.shouldShutdown).toBe(false);
  });

  it('defaults daemon callTool and listTools requests to cached auth', async () => {
    const runtime = createRuntimeDouble();
    const managedServers = createManagedServers();

    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
      id: 'call',
      method: 'callTool',
      params: { server: 'oauth', tool: 'ping' },
    });

    expect(runtime.callTool).toHaveBeenCalledWith('oauth', 'ping', {
      args: {},
      timeoutMs: undefined,
      disableOAuth: false,
    });

    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
      id: 'list',
      method: 'listTools',
      params: { server: 'oauth', includeSchema: true },
    });

    expect(runtime.listTools).toHaveBeenCalledWith('oauth', {
      includeSchema: true,
      autoAuthorize: undefined,
      allowCachedAuth: true,
      disableOAuth: false,
    });
  });

  it('keeps stdio keep-alive listTools requests reusable when callers disable auto auth', async () => {
    const runtime = createRuntimeDouble();
    const managedServers = createManagedServers();

    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
      id: 'list',
      method: 'listTools',
      params: { server: 'local', includeSchema: true, autoAuthorize: false, allowCachedAuth: true },
    });

    expect(runtime.listTools).toHaveBeenCalledWith('local', {
      includeSchema: true,
      autoAuthorize: undefined,
      allowCachedAuth: true,
      disableOAuth: false,
    });
  });

  it('preserves HTTP listTools auto-auth opt out on daemon requests', async () => {
    const runtime = createRuntimeDouble();
    const managedServers = createManagedServers();

    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
      id: 'list',
      method: 'listTools',
      params: { server: 'oauth', includeSchema: true, autoAuthorize: false, allowCachedAuth: true },
    });

    expect(runtime.listTools).toHaveBeenCalledWith('oauth', {
      includeSchema: true,
      autoAuthorize: false,
      allowCachedAuth: true,
      disableOAuth: false,
    });
  });

  it('forwards disableOAuth on daemon callTool and listTools requests', async () => {
    const runtime = createRuntimeDouble();
    const managedServers = createManagedServers();

    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
      id: 'call',
      method: 'callTool',
      params: { server: 'oauth', tool: 'ping', disableOAuth: true },
    });

    expect(runtime.callTool).toHaveBeenCalledWith('oauth', 'ping', {
      args: {},
      timeoutMs: undefined,
      disableOAuth: true,
    });

    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
      id: 'list',
      method: 'listTools',
      params: { server: 'oauth', includeSchema: true, disableOAuth: true },
    });

    expect(runtime.listTools).toHaveBeenCalledWith('oauth', {
      includeSchema: true,
      autoAuthorize: undefined,
      allowCachedAuth: true,
      disableOAuth: true,
    });
  });

  it('preserves explicit listTools cached-auth opt out on daemon requests', async () => {
    const runtime = createRuntimeDouble();
    const managedServers = createManagedServers();

    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
      id: 'list',
      method: 'listTools',
      params: { server: 'oauth', allowCachedAuth: false },
    });

    expect(runtime.listTools).toHaveBeenCalledWith('oauth', {
      includeSchema: undefined,
      autoAuthorize: undefined,
      allowCachedAuth: false,
      disableOAuth: false,
    });
  });
});

const describeUnixSocket = process.platform === 'win32' ? describe.skip : describe;

describeUnixSocket('isDaemonResponding', () => {
  const servers: net.Server[] = [];
  const connections: net.Socket[] = [];
  const socketPaths: string[] = [];

  function socketPath(): string {
    const p = path.join(os.tmpdir(), `mcporter-probe-${randomUUID().slice(0, 8)}.sock`);
    socketPaths.push(p);
    return p;
  }

  function listen(server: net.Server, p: string): Promise<void> {
    servers.push(server);
    server.on('connection', (socket) => connections.push(socket));
    return new Promise((resolve) => server.listen(p, () => resolve()));
  }

  afterEach(async () => {
    for (const socket of connections.splice(0)) {
      socket.destroy();
    }
    for (const server of servers.splice(0)) {
      await new Promise<void>((resolve) => server.close(() => resolve()));
    }
    for (const p of socketPaths.splice(0)) {
      await fs.rm(p, { force: true }).catch(() => {});
    }
  });

  it('returns true when the socket answers status with a matching socket and live pid', async () => {
    const p = socketPath();
    await listen(statusServer({ pid: process.pid, socketPath: p }), p);
    expect(await isDaemonResponding(p)).toBe(true);
  });

  it('returns false when the socket accepts but never responds (hung daemon)', async () => {
    const p = socketPath();
    await listen(
      net.createServer((socket) => socket.pause()),
      p
    );
    expect(await isDaemonResponding(p)).toBe(false);
  }, 5_000);

  it('returns false when status reports a different socket (foreign listener)', async () => {
    const p = socketPath();
    await listen(statusServer({ pid: process.pid, socketPath: '/some/other/daemon.sock' }), p);
    expect(await isDaemonResponding(p)).toBe(false);
  });

  it('returns false when status reports a dead pid', async () => {
    const p = socketPath();
    await listen(statusServer({ pid: 2_147_483_646, socketPath: p }), p);
    expect(await isDaemonResponding(p)).toBe(false);
  });

  it('returns false when nothing is listening', async () => {
    expect(await isDaemonResponding(socketPath())).toBe(false);
  });
});

describe('metadataMatches', () => {
  let metadataPath: string;
  const live = { pid: 4321, socketPath: '/tmp/daemon.sock' };

  beforeEach(async () => {
    metadataPath = path.join(os.tmpdir(), `mcporter-meta-${randomUUID().slice(0, 8)}.json`);
  });

  afterEach(async () => {
    await fs.rm(metadataPath, { force: true }).catch(() => {});
  });

  it('matches when pid and socket agree', async () => {
    await fs.writeFile(metadataPath, JSON.stringify({ pid: 4321, socketPath: '/tmp/daemon.sock' }), 'utf8');
    expect(await metadataMatches(metadataPath, live)).toBe(true);
  });

  it('does not match a different pid', async () => {
    await fs.writeFile(metadataPath, JSON.stringify({ pid: 9999, socketPath: '/tmp/daemon.sock' }), 'utf8');
    expect(await metadataMatches(metadataPath, live)).toBe(false);
  });

  it('does not match when metadata is missing', async () => {
    expect(await metadataMatches(metadataPath, live)).toBe(false);
  });

  it('does not match when metadata is corrupt', async () => {
    await fs.writeFile(metadataPath, '{ not json', 'utf8');
    expect(await metadataMatches(metadataPath, live)).toBe(false);
  });
});

describe('daemon artifact cleanup', () => {
  let dir: string;
  let metadataPath: string;
  let socketPath: string;

  beforeEach(async () => {
    dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-cleanup-'));
    metadataPath = path.join(dir, 'daemon.json');
    socketPath = path.join(dir, 'daemon.sock');
    await fs.writeFile(socketPath, 'socket', 'utf8');
  });

  afterEach(async () => {
    await fs.rm(dir, { recursive: true, force: true });
  });

  it('removes artifacts still owned by the stopping daemon', async () => {
    await fs.writeFile(metadataPath, JSON.stringify({ pid: 4321, socketPath }), 'utf8');

    await cleanupDaemonArtifactsIfOwned({ metadataPath, socketPath }, 4321);

    await expect(fs.access(metadataPath)).rejects.toThrow();
    if (process.platform === 'win32') {
      await expect(fs.access(socketPath)).resolves.toBeUndefined();
    } else {
      await expect(fs.access(socketPath)).rejects.toThrow();
    }
  });

  it('preserves artifacts replaced by a newer daemon', async () => {
    await fs.writeFile(metadataPath, JSON.stringify({ pid: 9876, socketPath }), 'utf8');

    await cleanupDaemonArtifactsIfOwned({ metadataPath, socketPath }, 4321);

    await expect(fs.access(metadataPath)).resolves.toBeUndefined();
    await expect(fs.access(socketPath)).resolves.toBeUndefined();
  });
});

function createRuntimeDouble(): Pick<Runtime, 'callTool' | 'listTools'> {
  return {
    callTool: vi.fn().mockResolvedValue({ ok: true }),
    listTools: vi.fn().mockResolvedValue([]),
  };
}

function createManagedServers(): Map<string, ServerDefinition> {
  return new Map([
    [
      'local',
      {
        name: 'local',
        command: { kind: 'stdio', command: 'node', args: ['server.js'], cwd: '/tmp' },
        lifecycle: { mode: 'keep-alive' },
      },
    ],
    [
      'oauth',
      {
        name: 'oauth',
        command: { kind: 'http', url: new URL('https://oauth.example.com/mcp') },
        lifecycle: { mode: 'keep-alive' },
      },
    ],
  ]);
}

function statusServer(result: Record<string, unknown>): net.Server {
  return net.createServer((socket) => {
    socket.on('data', () => socket.end(JSON.stringify({ id: '1', ok: true, result })));
  });
}
