/**
 * uat-plan/generate — assembly + serialization + the BYTE-STABLE golden plan.
 *
 * The golden test pins `generatePlan(personas.json)` to `expected.plantest.yml`.
 * To refresh after an intentional schema/format change: run vitest with
 * UPDATE_GOLDEN=1 (rewrites the fixture), eyeball the diff, then re-run clean.
 */
import { describe, it, expect } from 'vitest';
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { load } from 'js-yaml';
import {
  assemblePlan,
  serializePlan,
  computeSignature,
  emitArtifacts,
  generatePlan,
} from '../generate.js';
import type { Route } from '../../lib/plantest-schema.js';
import type { DiscoveryResult } from '../types.js';

const here = dirname(fileURLToPath(import.meta.url));
const fixtures = join(here, 'fixtures');
const SIG = { nav_sha: 'n', rbac_sha: 'r', registry_sha: 'g' };

function loadDiscovery(): DiscoveryResult {
  return JSON.parse(readFileSync(join(fixtures, 'personas.json'), 'utf8')) as DiscoveryResult;
}

describe('assemblePlan', () => {
  it('fills schema defaults and validates clean', () => {
    const { plan, violations } = assemblePlan({
      application: 'a',
      path: 'a',
      roles: ['admin', 'anonymous'],
      routes: [],
      endpoints: [],
      signature: SIG,
    });
    expect(violations).toEqual([]);
    expect(plan.execution.caps.tabs).toBe(8);
    expect(plan.execution.readiness.strategy).toBe('fully_ready');
    expect(plan.execution.perf.slow_ms).toBe(8000);
    expect(plan.drift_policy.signature_mismatch.fail_run).toBe(true);
    expect(plan.meta.generated_at).toBeUndefined();
    // anonymous present => per_role redirect rule added
    expect(plan.execution.per_role.anonymous).toEqual({ mode: 'goto', expect: 'redirect_login' });
  });

  it('a partial caps override keeps the other caps at their defaults', () => {
    const { plan } = assemblePlan({
      application: 'a',
      path: 'a',
      roles: ['admin'],
      routes: [],
      endpoints: [],
      signature: SIG,
      caps: { tabs: 2 },
    });
    expect(plan.execution.caps.tabs).toBe(2);
    expect(plan.execution.caps.buttons).toBe(10);
    // no anonymous => no per_role entry
    expect(plan.execution.per_role).toEqual({});
  });

  it('includes generated_at only when provided', () => {
    const { plan } = assemblePlan({
      application: 'a',
      path: 'a',
      roles: ['admin'],
      routes: [],
      endpoints: [],
      signature: SIG,
      generatedAt: '2026-06-04T00:00:00.000Z',
    });
    expect(plan.meta.generated_at).toBe('2026-06-04T00:00:00.000Z');
  });

  it('returns an invariant violation (not a throw) for an access map missing a role', () => {
    const route: Route = {
      id: 'R',
      route: '/r',
      navigation_strategy: 'menu_click',
      expect: { access_by_role: true, no_console_error: true },
      access: { admin: 'allowed' },
      subsets: [],
    };
    const { violations } = assemblePlan({
      application: 'a',
      path: 'a',
      roles: ['admin', 'viewer'],
      routes: [route],
      endpoints: [],
      signature: SIG,
    });
    expect(violations.some((v) => v.invariant === 3)).toBe(true);
  });
});

describe('serializePlan', () => {
  it('is idempotent and round-trips through YAML', () => {
    const { plan } = assemblePlan({
      application: 'a',
      path: 'a',
      roles: ['admin', 'anonymous'],
      routes: [],
      endpoints: [],
      signature: SIG,
    });
    const y1 = serializePlan(plan);
    expect(serializePlan(plan)).toBe(y1);
    expect(load(y1)).toEqual(plan);
  });
});

describe('computeSignature', () => {
  it('is stable and changes when any source part changes', () => {
    const a = computeSignature({ nav: 'x', rbac: 'y', registry: 'z' });
    expect(computeSignature({ nav: 'x', rbac: 'y', registry: 'z' })).toEqual(a);
    expect(a.nav_sha).toHaveLength(64);
    expect(computeSignature({ nav: 'x2', rbac: 'y', registry: 'z' }).nav_sha).not.toBe(a.nav_sha);
  });
});

describe('emitArtifacts', () => {
  it('emits the plan + signature sidecar with correct names and a trimmed outDir', () => {
    const files = emitArtifacts('administration', '.application-test/uat/administration/', 'yaml-here', SIG);
    expect(files.map((f) => f.path)).toEqual([
      '.application-test/uat/administration/administration.plantest.yml',
      '.application-test/uat/administration/administration.plantest.signature',
    ]);
    expect(files[0].content).toBe('yaml-here');
    expect(JSON.parse(files[1].content)).toEqual(SIG);
  });
});

describe('generatePlan — byte-stable golden + scope', () => {
  it('matches the golden plan (personas.json → expected.plantest.yml)', () => {
    const r = generatePlan(loadDiscovery(), {
      name: 'administration',
      outDir: '.application-test/uat/administration',
      modes: ['bfs', 'goto'],
      includeApi: true,
      generatedAt: '2026-06-04T00:00:00.000Z',
    });
    expect(r.violations).toEqual([]);
    const goldenPath = join(fixtures, 'expected.plantest.yml');
    if (process.env.UPDATE_GOLDEN) writeFileSync(goldenPath, r.yaml, 'utf8');
    // Normalise CRLF → LF so a git autocrlf checkout of the fixture can't break the
    // comparison; the generator only ever emits LF.
    const golden = readFileSync(goldenPath, 'utf8').replace(/\r\n/g, '\n');
    expect(r.yaml).toBe(golden);
  });

  it('expands the path sub-tree into the expected route ids', () => {
    const r = generatePlan(loadDiscovery(), { name: 'x', outDir: 'd', includeApi: true });
    expect(r.plan.routes.map((x) => x.id)).toEqual([
      'ADMINISTRATION_USERS',
      'ADMINISTRATION_USERS_DETAIL',
      'ADMINISTRATION_USERS_CREATE',
      'ADMINISTRATION_ROLES',
    ]);
  });

  it('canonicalises the scrambled fixture roles', () => {
    const r = generatePlan(loadDiscovery(), { name: 'x', outDir: 'd', includeApi: true });
    expect(r.plan.roles).toEqual(['super-admin', 'admin', 'manager', 'contributor', 'viewer', 'anonymous']);
  });

  it('includeApi:false omits the endpoints axis', () => {
    const r = generatePlan(loadDiscovery(), { name: 'x', outDir: 'd', includeApi: false });
    expect(r.plan.endpoints).toEqual([]);
  });
});
