import { spawn, type ChildProcess } from 'node:child_process';
import { readFile, rm } from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';

import {
  getSupervisorManagementStatus,
  resolveAutomationControlConfig,
  type AutomationSupervisorState,
} from './dev-supervisor-state';

describe('dev supervisor state helpers', () => {
  it('rejects conflicting Vite and supervisor automation ports', () => {
    const config = resolveAutomationControlConfig({
      VITE_AUTOMATION_PORT: '3031',
      AUTOMATION_PORT: '3030',
    }, '/workspace/project');

    expect(config.port).toBe('3031');
    expect(config.portConfigError).toContain('VITE_AUTOMATION_PORT=3031');
    expect(config.restartSignalFile).toContain('restart-3031.signal');
  });

  it('treats a live supervisor state as managed', () => {
    const state: AutomationSupervisorState = {
      port: 3030,
      status: 'starting',
      supervisorPid: 12345,
      childPid: 12346,
      updatedAt: '2026-04-26T10:00:00.000Z',
    };

    const status = getSupervisorManagementStatus(state, {
      isProcessAlive: (pid) => pid === 12345,
    });

    expect(status.managed).toBe(true);
    expect(status.reason).toContain('supervisor');
  });

  it('treats occupied ports with no live supervisor as unmanaged', () => {
    const state: AutomationSupervisorState = {
      port: 3030,
      status: 'unmanaged',
      supervisorPid: 22222,
      updatedAt: '2026-04-26T10:00:00.000Z',
      reason: 'port already occupied',
    };

    const status = getSupervisorManagementStatus(state, {
      isProcessAlive: () => false,
    });

    expect(status.managed).toBe(false);
    expect(status.reason).toContain('不归当前 supervisor 管理');
  });

  it('records unmanaged state when the supervisor starts on an occupied port', async () => {
    const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
    const port = await listenOnRandomLocalPort();
    const stateFile = path.join(projectRoot, '.runtime', 'automation-control', `state-${port}.json`);
    const signalFile = path.join(projectRoot, '.runtime', 'automation-control', `restart-${port}.signal`);
    let supervisor: ChildProcess | null = null;

    try {
      supervisor = spawn(process.execPath, ['scripts/ensure-automation-api.mjs'], {
        cwd: projectRoot,
        env: {
          ...process.env,
          AUTOMATION_DEV_HOST: '127.0.0.1',
          AUTOMATION_PORT: String(port),
        },
        stdio: ['ignore', 'pipe', 'pipe'],
      });

      const rawState = await waitForFileText(stateFile);
      const state = JSON.parse(rawState) as AutomationSupervisorState;

      expect(state.status).toBe('unmanaged');
      expect(state.port).toBe(port);
      expect(state.reason).toContain('port already occupied');
    } finally {
      supervisor?.kill('SIGTERM');
      await closeRandomPortServer();
      await rm(stateFile, { force: true });
      await rm(signalFile, { force: true });
    }
  });
});

let randomPortServer: net.Server | null = null;

async function listenOnRandomLocalPort() {
  randomPortServer = net.createServer();
  await new Promise<void>((resolve, reject) => {
    randomPortServer?.once('error', reject);
    randomPortServer?.listen(0, '127.0.0.1', () => resolve());
  });

  const address = randomPortServer.address();
  if (!address || typeof address === 'string') {
    throw new Error('Failed to allocate a local TCP port');
  }

  return address.port;
}

async function closeRandomPortServer() {
  if (!randomPortServer) {
    return;
  }

  await new Promise<void>((resolve, reject) => {
    randomPortServer?.close((error) => {
      if (error) {
        reject(error);
        return;
      }

      resolve();
    });
  });
  randomPortServer = null;
}

async function waitForFileText(filePath: string) {
  const startedAt = Date.now();
  let lastError: unknown;

  while (Date.now() - startedAt < 3000) {
    try {
      return await readFile(filePath, 'utf8');
    } catch (error) {
      lastError = error;
      await new Promise((resolve) => setTimeout(resolve, 50));
    }
  }

  throw lastError instanceof Error ? lastError : new Error(`Timed out waiting for ${filePath}`);
}
