import { createServer, type Server } from 'node:net';
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { afterEach, describe, expect, it } from 'vitest';

import {
  applyBrokerSupervisor,
  checkBrokerSupervisorHealth,
  isBrokerSupervisorHealthy,
  resolveBrokerSupervisorPaths,
  resolveLeaseBrokerSocketPath,
  type BrokerSupervisorPaths,
} from './broker-supervisor.js';

const REAL_FRAMEWORK_ROOT = new URL('../../framework/', import.meta.url).pathname;

const cleanupDirs: string[] = [];
const cleanupServers: Server[] = [];

async function tempDir(prefix: string): Promise<string> {
  const dir = await mkdtemp(join(tmpdir(), prefix));
  cleanupDirs.push(dir);
  return dir;
}

afterEach(async () => {
  for (const server of cleanupServers.splice(0)) {
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }
  for (const dir of cleanupDirs.splice(0)) {
    await rm(dir, { recursive: true, force: true });
  }
});

describe('resolveLeaseBrokerSocketPath', () => {
  it('honors an explicit MOSAIC_LEASE_BROKER_SOCKET override', () => {
    expect(resolveLeaseBrokerSocketPath({ MOSAIC_LEASE_BROKER_SOCKET: '/tmp/explicit.sock' })).toBe(
      '/tmp/explicit.sock',
    );
  });

  it('falls back to $XDG_RUNTIME_DIR/mosaic-lease/broker.sock', () => {
    expect(resolveLeaseBrokerSocketPath({ XDG_RUNTIME_DIR: '/run/user/1000' })).toBe(
      join('/run/user/1000', 'mosaic-lease', 'broker.sock'),
    );
  });

  it('falls back to /run/user/<uid>/mosaic-lease/broker.sock as a last resort', () => {
    expect(resolveLeaseBrokerSocketPath({}, 4242)).toBe(
      join('/run/user', '4242', 'mosaic-lease', 'broker.sock'),
    );
  });

  it('prefers the explicit override over XDG_RUNTIME_DIR', () => {
    expect(
      resolveLeaseBrokerSocketPath({
        MOSAIC_LEASE_BROKER_SOCKET: '/explicit.sock',
        XDG_RUNTIME_DIR: '/run/user/1000',
      }),
    ).toBe('/explicit.sock');
  });
});

describe('resolveBrokerSupervisorPaths', () => {
  it('colocates the state file next to the resolved socket', () => {
    const paths = resolveBrokerSupervisorPaths({
      mosaicHome: '/home/x/.config/mosaic',
      frameworkRoot: '/repo/framework',
      env: { XDG_RUNTIME_DIR: '/run/user/1000' },
    });
    expect(paths.socketPath).toBe(join('/run/user/1000', 'mosaic-lease', 'broker.sock'));
    expect(paths.statePath).toBe(join('/run/user/1000', 'mosaic-lease', 'state.json'));
  });

  it('targets the systemd --user dir under the given home, not mosaicHome', () => {
    const paths = resolveBrokerSupervisorPaths({
      mosaicHome: '/somewhere-else/.config/mosaic',
      frameworkRoot: '/repo/framework',
      homeDir: '/home/canary',
      env: {},
      uid: 0,
    });
    expect(paths.systemdUserDir).toBe(join('/home/canary', '.config', 'systemd', 'user'));
    expect(paths.unitTargetPath).toBe(
      join('/home/canary', '.config', 'systemd', 'user', 'mosaic-lease-broker.service'),
    );
  });

  it('is a pure function: identical options resolve to identical paths', () => {
    const options = {
      mosaicHome: '/h/.config/mosaic',
      frameworkRoot: '/repo/framework',
      env: { XDG_RUNTIME_DIR: '/run/user/1000' },
    };
    expect(resolveBrokerSupervisorPaths(options)).toEqual(resolveBrokerSupervisorPaths(options));
  });
});

describe('applyBrokerSupervisor', () => {
  async function fakePaths(): Promise<BrokerSupervisorPaths> {
    const home = await tempDir('mosaic-broker-supervisor-home-');
    const mosaicHome = join(home, '.config', 'mosaic');
    const runtimeDir = await tempDir('mosaic-broker-supervisor-runtime-');
    return resolveBrokerSupervisorPaths({
      mosaicHome,
      frameworkRoot: REAL_FRAMEWORK_ROOT,
      homeDir: home,
      env: { XDG_RUNTIME_DIR: runtimeDir },
    });
  }

  it('renders a unit that references the installed wrapper script and hardens the runtime dir', async () => {
    const paths = await fakePaths();
    const unitSource = await readFile(paths.unitSourcePath, 'utf8');
    expect(unitSource).toContain('ExecStart=');
    expect(unitSource).toContain('%h/.config/mosaic/tools/lease-broker/start-lease-broker.sh');
    expect(unitSource).toContain('RuntimeDirectory=mosaic-lease');
    expect(unitSource).toContain('RuntimeDirectoryMode=0700');
    expect(unitSource).toContain('Restart=on-failure');
    expect(unitSource).toContain('WantedBy=default.target');
    // No ambient environment file preload, matching the other fleet units'
    // strict-parsing convention.
    expect(unitSource).not.toMatch(/^Environment(File)?=/m);
  });

  it('materializes the unit, wrapper script, and daemon sources on first apply', async () => {
    const paths = await fakePaths();

    const result = await applyBrokerSupervisor(paths);

    expect(result.installedFiles).toContain(paths.unitTargetPath);
    expect(result.installedFiles).toContain(paths.wrapperTargetPath);
    for (const target of paths.daemonTargetPaths) {
      expect(result.installedFiles).toContain(target);
    }

    const unitTargetContent = await readFile(paths.unitTargetPath, 'utf8');
    const unitSourceContent = await readFile(paths.unitSourcePath, 'utf8');
    expect(unitTargetContent).toBe(unitSourceContent);

    const wrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777;
    expect(wrapperMode).toBe(0o755);

    for (const target of paths.daemonTargetPaths) {
      await expect(stat(target)).resolves.toBeDefined();
    }
  });

  it('is idempotent: applying twice reproduces identical files with no error', async () => {
    const paths = await fakePaths();

    await applyBrokerSupervisor(paths);
    const firstUnit = await readFile(paths.unitTargetPath, 'utf8');
    const firstWrapper = await readFile(paths.wrapperTargetPath, 'utf8');
    const firstWrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777;

    await expect(applyBrokerSupervisor(paths)).resolves.toBeDefined();

    const secondUnit = await readFile(paths.unitTargetPath, 'utf8');
    const secondWrapper = await readFile(paths.wrapperTargetPath, 'utf8');
    const secondWrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777;

    expect(secondUnit).toBe(firstUnit);
    expect(secondWrapper).toBe(firstWrapper);
    expect(secondWrapperMode).toBe(firstWrapperMode);
  });

  it('never touches the real host: only writes under the supplied temp dirs', async () => {
    const paths = await fakePaths();
    await applyBrokerSupervisor(paths);
    expect(paths.systemdUserDir.startsWith(tmpdir())).toBe(true);
    expect(paths.mosaicHome.startsWith(tmpdir())).toBe(true);
  });
});

describe('checkBrokerSupervisorHealth / isBrokerSupervisorHealthy', () => {
  async function fakeHealthPaths(): Promise<
    Pick<BrokerSupervisorPaths, 'unitTargetPath' | 'socketPath'>
  > {
    const runtimeDir = await tempDir('mosaic-broker-supervisor-health-');
    await mkdir(join(runtimeDir, 'systemd-user'), { recursive: true });
    return {
      unitTargetPath: join(runtimeDir, 'systemd-user', 'mosaic-lease-broker.service'),
      socketPath: join(runtimeDir, 'broker.sock'),
    };
  }

  it('reports unhealthy when neither the unit nor the socket exist', async () => {
    const paths = await fakeHealthPaths();

    const health = await checkBrokerSupervisorHealth(paths);

    expect(health).toEqual({ unitInstalled: false, socketPresent: false, healthy: false });
    expect(await isBrokerSupervisorHealthy(paths)).toBe(false);
  });

  it('reports unhealthy when the unit is installed but no socket is listening', async () => {
    const paths = await fakeHealthPaths();
    await writeFile(paths.unitTargetPath, '[Unit]\n');

    const health = await checkBrokerSupervisorHealth(paths);

    expect(health.unitInstalled).toBe(true);
    expect(health.socketPresent).toBe(false);
    expect(health.healthy).toBe(false);
  });

  it('reports healthy=true once a real Unix socket exists at the resolved path, and false again once removed', async () => {
    const paths = await fakeHealthPaths();

    const server = createServer();
    cleanupServers.push(server);
    await new Promise<void>((resolve, reject) => {
      server.once('error', reject);
      server.listen(paths.socketPath, resolve);
    });

    expect(await isBrokerSupervisorHealthy(paths)).toBe(true);
    const health = await checkBrokerSupervisorHealth(paths);
    expect(health.socketPresent).toBe(true);
    expect(health.healthy).toBe(true);

    await new Promise<void>((resolve) => server.close(() => resolve()));
    await rm(paths.socketPath, { force: true });

    expect(await isBrokerSupervisorHealthy(paths)).toBe(false);
  });

  it('does not confuse a stale regular file at the socket path with a live socket', async () => {
    const paths = await fakeHealthPaths();
    await writeFile(paths.socketPath, 'not actually a socket');

    expect(await isBrokerSupervisorHealthy(paths)).toBe(false);
  });
});
