/**
 * plantest-schema — pure-layer tests.
 *
 * Drives the real parsePlan()/validateInvariants() against a known-good plan and
 * targeted mutations, so the `.plantest.yml` contract + the 8 structural
 * invariants are checked against the actual parser a developer would run.
 */
import { describe, it, expect } from 'vitest';
import { parsePlan, validateInvariants, PlanTestSchema, type PlanTest } from '../plantest-schema.js';

/** Minimal schema-valid plan (input shape — defaults fill the rest). */
function validInput(): Record<string, unknown> {
  return {
    meta: { application: 'administration', path: 'administration/users' },
    roles: ['super-admin', 'admin', 'manager', 'anonymous'],
    execution: { screenshots: { out_dir: '.application-test/runs/x/screenshots' } },
    routes: [
      {
        id: 'USERS_LIST',
        component_key: 'administration.users',
        route: '/administration/users',
        permission: 'administration.users.read',
        access: { 'super-admin': 'allowed', admin: 'allowed', manager: 'allowed', anonymous: 'redirect_login' },
        subsets: [
          {
            id: 'btn_create',
            type: 'create',
            kind: 'button',
            action_kind: 'open_modal',
            selector: 'button:has-text("Create")',
            modal_indicator: '[role="dialog"]',
            visible_for: ['super-admin', 'admin'],
            expect: 'modal_opens_then_closes',
          },
        ],
      },
    ],
    endpoints: [
      {
        id: 'post:administration/users',
        method: 'POST',
        route: '/api/administration/users',
        permission: 'administration.users.create',
        expected_by_role: { 'super-admin': 200, admin: 200, manager: 403, anonymous: 401 },
      },
    ],
  };
}

function parsedValid(): PlanTest {
  const r = parsePlan(validInput());
  if (!r.ok) throw new Error(`fixture should parse: ${r.errors.join(', ')}`);
  return r.plan;
}

describe('parsePlan — happy path + defaults', () => {
  it('parses a valid plan with zero invariant violations', () => {
    const r = parsePlan(validInput());
    expect(r.ok).toBe(true);
    if (r.ok) expect(r.violations).toEqual([]);
  });

  it('applies schema defaults (schema_version, caps, readiness, perf, subset action defaults)', () => {
    const plan = parsedValid();
    expect(plan.schema_version).toBe('1.0.0');
    expect(plan.execution.caps.tabs).toBe(8);
    expect(plan.execution.caps.actions_per_role).toBe(500);
    expect(plan.execution.readiness.strategy).toBe('fully_ready');
    expect(plan.execution.perf.warn_ms).toBe(3000);
    expect(plan.execution.perf.slow_ms).toBe(8000);
    expect(plan.execution.modes).toEqual(['bfs', 'goto']);
    // drift defaults re-parsed (fail_run filled in).
    expect(plan.drift_policy.overexposed_subset).toEqual({ severity: 'BLOCKER', fail_run: false });
    expect(plan.drift_policy.signature_mismatch.fail_run).toBe(true);
  });

  it('rejects unknown access verdicts — `partial` is NEVER stored in the plan', () => {
    const bad = validInput();
    (bad.routes as Array<Record<string, unknown>>)[0].access = { 'super-admin': 'partial' };
    const r = parsePlan(bad);
    expect(r.ok).toBe(false);
  });

  it('requires execution.screenshots.out_dir', () => {
    const bad = validInput();
    (bad.execution as Record<string, unknown>).screenshots = {};
    const r = parsePlan(bad);
    expect(r.ok).toBe(false);
  });
});

describe('validateInvariants — each structural invariant fires', () => {
  function fires(invariant: number, mutate: (p: PlanTest) => void): void {
    const plan = parsedValid();
    mutate(plan);
    const nums = validateInvariants(plan).map((v) => v.invariant);
    expect(nums).toContain(invariant);
  }

  it('(1) route id must be UPPER_SNAKE', () => {
    fires(1, (p) => {
      p.routes[0].id = 'users_list';
    });
  });

  it('(2) subset ids unique within a route', () => {
    fires(2, (p) => {
      const s = p.routes[0].subsets[0];
      p.routes[0].subsets.push({ ...s });
    });
  });

  it('(2) endpoint ids unique', () => {
    fires(2, (p) => {
      p.endpoints.push({ ...p.endpoints[0] });
    });
  });

  it('(2) route ids unique', () => {
    fires(2, (p) => {
      p.routes.push({ ...p.routes[0] });
    });
  });

  it('(3) access must cover every role', () => {
    fires(3, (p) => {
      delete (p.routes[0].access as Record<string, unknown>)['manager'];
    });
  });

  it('(3) expected_by_role must cover every role', () => {
    fires(3, (p) => {
      delete (p.endpoints[0].expected_by_role as Record<string, unknown>)['manager'];
    });
  });

  it('(4) visible_for must be a subset of plan.roles', () => {
    fires(4, (p) => {
      p.routes[0].subsets[0].visible_for.push('ghost-role');
    });
  });

  it('(5) destructive subset must be action_kind log_only', () => {
    fires(5, (p) => {
      p.routes[0].subsets[0].is_destructive = true;
      p.routes[0].subsets[0].action_kind = 'click';
    });
  });

  it('(6) form subset requires non-empty fields[]', () => {
    fires(6, (p) => {
      p.routes[0].subsets[0].kind = 'form';
      p.routes[0].subsets[0].fields = undefined;
    });
  });

  it('(7) open_modal requires modal_indicator', () => {
    fires(7, (p) => {
      p.routes[0].subsets[0].action_kind = 'open_modal';
      p.routes[0].subsets[0].modal_indicator = undefined;
    });
  });

  it('(8) navigate requires expect_url or navigates_to', () => {
    fires(8, (p) => {
      p.routes[0].subsets[0].action_kind = 'navigate';
      p.routes[0].subsets[0].modal_indicator = undefined;
      p.routes[0].subsets[0].expect_url = undefined;
      p.routes[0].subsets[0].navigates_to = undefined;
    });
  });

  it('(8) click_row_in_parent requires parent_route', () => {
    fires(8, (p) => {
      p.routes[0].navigation_strategy = 'click_row_in_parent';
      p.routes[0].parent_route = undefined;
    });
  });
});

describe('PlanTestSchema — direct', () => {
  it('round-trips a valid plan via .parse', () => {
    expect(() => PlanTestSchema.parse(validInput())).not.toThrow();
  });
});
