import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

import { placeUnitFile, resolveLeaseBrokerSocketForPreflight } from './fleet.js';

/**
 * Unit-placement regression harness for #1292.
 *
 * The two measured defects this suite pins:
 *  1. `systemctl enable <name>` does NOT rewrite an existing by-path
 *     wants-symlink — so placement must remove stale residue explicitly, and
 *     acceptance asserts on the RESULTING SYMLINK TARGET, never on the enable
 *     call's argument (asserting the call cannot see where the link ended up).
 *  2. Node's copyFile FOLLOWS a by-path symlink at the destination and
 *     overwrites the SEED template. Acceptance asserts on the SEED's bytes
 *     AND mtime — unchanged — which is the only check that can redden for
 *     finding 2. The symlink-target assertion catches finding 1; these are
 *     different defects with different failure modes.
 *
 * Fixtures are entirely inside tmpdirs (source template, active systemd dir,
 * wants dir) — no real host paths are touched by this suite.
 */

describe('placeUnitFile (#1292 unit placement)', () => {
  const cleanup: string[] = [];
  afterEach(async () => {
    while (cleanup.length > 0) {
      await rm(cleanup.pop()!, { recursive: true, force: true });
    }
  });

  async function fixture() {
    const root = await mkdtemp(join(tmpdir(), 'place-unit-'));
    cleanup.push(root);
    const seedDir = join(root, 'seed');
    const activeDir = join(root, 'active');
    await mkdir(seedDir, { recursive: true });
    await mkdir(activeDir, { recursive: true });
    const seedTemplate = join(seedDir, 'unit-under-test.service');
    await writeFile(
      seedTemplate,
      '[Unit]\nDescription=seed template\n[Service]\nType=oneshot\nExecStart=/bin/true\n[Install]\nWantedBy=default.target\n',
    );
    const activeSource = join(root, 'active-source.service');
    await writeFile(
      activeSource,
      '[Unit]\nDescription=active copy v2\n[Service]\nType=oneshot\nExecStart=/bin/true\n[Install]\nWantedBy=default.target\n',
    );
    return { root, seedDir, activeDir, seedTemplate, activeSource };
  }

  it('places a regular file on a clean host (negative control: no residue anywhere)', async () => {
    const f = await fixture();
    const result = await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service');
    expect(result.unlinkedDestinationSymlink).toBe(false);
    expect(result.removedStaleWantsSymlink).toBe(false);
    const info = await lstat(join(f.activeDir, 'unit-under-test.service'));
    expect(info.isSymbolicLink()).toBe(false);
    expect(await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8')).toContain(
      'active copy v2',
    );
    // Seed untouched by construction — but assert it, so the clean-host case
    // cannot silently regress into seed-mutation.
    expect(await readFile(f.seedTemplate, 'utf8')).toContain('seed template');
  });

  it('by-path residue: unlinks destination symlink, places the file, seed bytes AND mtime unchanged (finding 2)', async () => {
    const f = await fixture();
    const seedBefore = await readFile(f.seedTemplate, 'utf8');
    const mtimeBefore = (await lstat(f.seedTemplate)).mtimeMs;
    // The fomo-lin convention: by-path enable left a symlink AT the unit name
    // pointing at the seed template, plus a wants-symlink doing the same.
    await symlink(f.seedTemplate, join(f.activeDir, 'unit-under-test.service'));
    const wantsDir = join(f.activeDir, 'default.target.wants');
    await mkdir(wantsDir, { recursive: true });
    await symlink(f.seedTemplate, join(wantsDir, 'unit-under-test.service'));

    const result = await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service');
    expect(result.unlinkedDestinationSymlink).toBe(true);
    expect(result.removedStaleWantsSymlink).toBe(true);

    // FINDING 2's check: the seed is byte-identical and its mtime did not move.
    expect(await readFile(f.seedTemplate, 'utf8')).toBe(seedBefore);
    expect((await lstat(f.seedTemplate)).mtimeMs).toBe(mtimeBefore);

    // The destination is now a regular file carrying the ACTIVE content.
    const destInfo = await lstat(join(f.activeDir, 'unit-under-test.service'));
    expect(destInfo.isSymbolicLink()).toBe(false);
    expect(await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8')).toContain(
      'active copy v2',
    );
  });

  it('by-path residue: no wants-symlink remains pointing at the seed (finding 1 residue cleared)', async () => {
    const f = await fixture();
    await symlink(f.seedTemplate, join(f.activeDir, 'unit-under-test.service'));
    const wantsDir = join(f.activeDir, 'default.target.wants');
    await mkdir(wantsDir, { recursive: true });
    await symlink(f.seedTemplate, join(wantsDir, 'unit-under-test.service'));

    await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service');

    // After placement the stale wants link is GONE (enable-by-name recreates
    // it correctly). A link still present must not point at the seed.
    try {
      const link = await lstat(join(wantsDir, 'unit-under-test.service'));
      if (link.isSymbolicLink()) {
        const target = await readFile(join(wantsDir, 'unit-under-test.service'), 'utf8').catch(
          async () => '',
        );
        expect(target).not.toContain('seed template');
      }
    } catch {
      // absent wants link — the expected post-placement state
    }
  });

  it('idempotence: second placement on a reconciled host is a no-op producing the identical final state', async () => {
    const f = await fixture();
    // Reconciled starting state: regular file at the name, wants link to the active copy.
    await writeFile(
      join(f.activeDir, 'unit-under-test.service'),
      await readFile(f.activeSource, 'utf8'),
    );
    const wantsDir = join(f.activeDir, 'default.target.wants');
    await mkdir(wantsDir, { recursive: true });
    await symlink(
      join(f.activeDir, 'unit-under-test.service'),
      join(wantsDir, 'unit-under-test.service'),
    );
    const before = await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8');

    const result = await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service');
    // No destructive step fired: no unlink, no wants removal.
    expect(result.unlinkedDestinationSymlink).toBe(false);
    expect(result.removedStaleWantsSymlink).toBe(false);
    // Identical final state.
    expect(await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8')).toBe(before);
    const link = await lstat(join(wantsDir, 'unit-under-test.service'));
    expect(link.isSymbolicLink()).toBe(true);
  });

  it('double install on by-path residue converges to the identical reconciled state', async () => {
    const f = await fixture();
    await symlink(f.seedTemplate, join(f.activeDir, 'unit-under-test.service'));
    const wantsDir = join(f.activeDir, 'default.target.wants');
    await mkdir(wantsDir, { recursive: true });
    await symlink(f.seedTemplate, join(wantsDir, 'unit-under-test.service'));

    await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service');
    const first = await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8');
    const secondRun = await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service');
    const second = await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8');
    expect(secondRun.unlinkedDestinationSymlink).toBe(false);
    expect(second).toBe(first);
  });
});

describe('resolveLeaseBrokerSocketForPreflight (#1292 preflight resolution)', () => {
  it('explicit MOSAIC_LEASE_BROKER_SOCKET wins', () => {
    expect(
      resolveLeaseBrokerSocketForPreflight({ MOSAIC_LEASE_BROKER_SOCKET: '/custom/sock' }, 1000),
    ).toBe('/custom/sock');
  });
  it('XDG_RUNTIME_DIR next', () => {
    expect(resolveLeaseBrokerSocketForPreflight({ XDG_RUNTIME_DIR: '/run/user/1001' }, 1000)).toBe(
      '/run/user/1001/mosaic-lease/broker.sock',
    );
  });
  it('falls back to /run/user/<uid>', () => {
    expect(resolveLeaseBrokerSocketForPreflight({}, 1002)).toBe(
      '/run/user/1002/mosaic-lease/broker.sock',
    );
  });
});
