import assert from 'node:assert/strict';
import { access, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';

import { buildWeb } from './build-web.mjs';

const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `build-web-${process.pid}`);

async function fixture(name) {
  const root = path.join(fixtureRoot, name);
  await mkdir(path.join(root, 'apps', 'web', '.next'), { recursive: true });
  return root;
}

async function exists(target) {
  try {
    await access(target);
    return true;
  } catch {
    return false;
  }
}

test.after(async () => {
  await rm(fixtureRoot, { recursive: true, force: true });
});

test('a successful web build atomically publishes its source and symlink certification', async () => {
  const root = await fixture('success');
  const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
  const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');

  await buildWeb({ root, fingerprint: async () => 'certified', runBuild: async () => {} });

  assert.deepEqual(JSON.parse(await readFile(marker, 'utf8')), {
    version: 1,
    sourceFingerprint: 'certified',
    symlinkManifestHash: '8a5a375cea6a55d24bd5f875856da63feba33adbefb15a92a0007719b84bcf11',
  });
  assert.equal(await readFile(manifest, 'utf8'), '{"version":1,"links":[]}\n');
});

test('a failed web build leaves no certification marker', async () => {
  const root = await fixture('failure');
  const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
  const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
  await writeFile(marker, 'stale\n');
  await writeFile(manifest, 'stale\n');

  await assert.rejects(
    buildWeb({
      root,
      fingerprint: async () => 'before',
      runBuild: async () => {
        throw new Error('build failed');
      },
    }),
    /build failed/,
  );

  assert.equal(await exists(marker), false);
  assert.equal(await exists(manifest), false);
});

test('overlapping web builds are serialized while the marker remains absent', async () => {
  const root = await fixture('overlap');
  const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
  const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
  await writeFile(marker, 'stale\n');
  await writeFile(manifest, 'stale\n');
  let releaseFirst;
  let secondEntered = false;
  const firstEntered = new Promise((resolve) => {
    releaseFirst = resolve;
  });
  let markFirstEntered;
  const firstStarted = new Promise((resolve) => {
    markFirstEntered = resolve;
  });

  const first = buildWeb({
    root,
    fingerprint: async () => 'certified',
    runBuild: async () => {
      markFirstEntered();
      await firstEntered;
    },
  });
  await firstStarted;
  const second = buildWeb({
    root,
    fingerprint: async () => 'certified',
    runBuild: async () => {
      secondEntered = true;
    },
  });
  await new Promise((resolve) => setTimeout(resolve, 75));
  assert.equal(secondEntered, false);
  assert.equal(await exists(marker), false);
  assert.equal(await exists(manifest), false);

  releaseFirst();
  await Promise.all([first, second]);
  assert.equal(secondEntered, true);
  assert.equal(JSON.parse(await readFile(marker, 'utf8')).sourceFingerprint, 'certified');
  assert.equal(await readFile(manifest, 'utf8'), '{"version":1,"links":[]}\n');
});

test('a build that replaces .next with a symbolic link cannot publish outside the checkout', async () => {
  const root = await fixture('symbolic-next');
  const nextDir = path.join(root, 'apps', 'web', '.next');
  const outside = path.join(root, 'outside-generated');
  await mkdir(outside);

  await assert.rejects(
    buildWeb({
      root,
      fingerprint: async () => 'certified',
      runBuild: async () => {
        await rm(nextDir, { recursive: true });
        await symlink(outside, nextDir);
      },
    }),
    /must be a real directory/,
  );

  assert.equal(await exists(path.join(outside, '.mosaic-source-hash')), false);
  assert.equal(await exists(path.join(outside, '.mosaic-symlink-manifest')), false);
});

test('inputs changed during a web build are not certified', async () => {
  const root = await fixture('changed-inputs');
  const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
  const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
  const fingerprints = ['before', 'after'];

  await assert.rejects(
    buildWeb({
      root,
      fingerprint: async () => fingerprints.shift(),
      runBuild: async () => {},
    }),
    /inputs changed during next build/,
  );

  assert.equal(await exists(marker), false);
  assert.equal(await exists(manifest), false);
});
