/**
 * Phase 2 — prisma controller-generator emits guards import + extended
 * validate() body when the model has declared constraints.
 *
 * This is a smoke-shape test: we assert STRINGS in the generated controller
 * source, not runtime behavior of the controller. The runtime behavior is
 * covered by the end-to-end realize integration test (Task #8).
 */

import { describe, it, expect } from 'vitest';
import generatePrismaController from '../controller-generator.js';
import type { ModelConstraintSpec } from '@specverse/types';

function buildConstrainedModel(): any {
  const expanded: ModelConstraintSpec = {
    on: ['create'],
    requires: {
      type: 'guard',
      name: 'Vote_create_requires',
      body: 'self.poll.votingStatus == "open"',
      params: 'self: Vote, actor: User',
      source: {
        convention: 'compound',
        entity: 'constraints',
        input: 'self.poll.votingStatus == "open"',
      },
    },
    source: {
      authorOn: 'create',
      authorRequires: 'self.poll.votingStatus == "open"',
    },
  };
  return {
    name: 'Vote',
    attributes: [
      { name: 'id', type: 'UUID', required: true, unique: true, category: 'metadata', auto: 'uuid4' },
      { name: 'choice', type: 'String', required: true, unique: false, category: 'business' },
    ],
    relationships: [],
    lifecycles: [],
    behaviors: {},
    constraints: [expanded],
  };
}

function buildUnconstrainedModel(): any {
  return {
    name: 'Comment',
    attributes: [
      { name: 'id', type: 'UUID', required: true, unique: true, category: 'metadata', auto: 'uuid4' },
      { name: 'text', type: 'String', required: true, unique: false, category: 'business' },
    ],
    relationships: [],
    lifecycles: [],
    behaviors: {},
  };
}

function buildController(modelName: string): any {
  return {
    name: `${modelName}Controller`,
    model: modelName,
    modelReference: modelName,
    cured: { create: {}, retrieve: {}, update: {}, validate: {}, delete: {} },
  };
}

describe('Phase 2 — controller-generator with model.constraints', () => {
  it('emits import for guards module when model has constraints', () => {
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildConstrainedModel(),
      controller: buildController('Vote'),
      models: [buildConstrainedModel()],
    });
    expect(code).toContain(`import { runGuards as runConstraintGuards } from './Vote.guards.js';`);
  });

  it('does NOT emit guards import for unconstrained models', () => {
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildUnconstrainedModel(),
      controller: buildController('Comment'),
      models: [buildUnconstrainedModel()],
    });
    expect(code).not.toContain('.guards.js');
    expect(code).not.toContain('runConstraintGuards');
  });

  it('extends validate() signature with actor + widens op union', () => {
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildConstrainedModel(),
      controller: buildController('Vote'),
      models: [buildConstrainedModel()],
    });
    expect(code).toContain('_actor: any = null');
    // Wider union includes 'delete' and the template-literal evolve.<X>.
    expect(code).toContain(`'delete'`);
    expect(code).toContain('`evolve.${string}`');
  });

  it('emits runConstraintGuards call inside validate() body', () => {
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildConstrainedModel(),
      controller: buildController('Vote'),
      models: [buildConstrainedModel()],
    });
    expect(code).toContain('await runConstraintGuards(_data, _context.operation, _actor, __guardCtx)');
  });

  // TODO #76 — flow-shorthand lifecycles (`flow: draft -> open -> closed`)
  // carry no explicit `states` array, which previously gated OUT the evolve
  // transition-validation block, so the realized backend accepted any
  // state→state jump (e.g. draft→closed). states is now derived from the
  // flow-aware transition map.
  it('enforces flow-shorthand lifecycle transitions in evolve', () => {
    const model: any = {
      name: 'Poll',
      attributes: [
        { name: 'id', type: 'UUID', required: true, unique: true, category: 'metadata', auto: 'uuid4' },
        { name: 'title', type: 'String', required: true, unique: false, category: 'business' },
        { name: 'status', type: 'String', required: true, unique: false, category: 'business' },
      ],
      relationships: [],
      lifecycles: { status: { flow: 'draft -> open -> closed' } },
      behaviors: {},
    };
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model,
      controller: { name: 'PollController', model: 'Poll', modelReference: 'Poll', cured: { create: {}, retrieve: {}, update: {}, evolve: {}, delete: {} } },
      models: [model],
    });
    // The transition-validation block must now be emitted...
    expect(code).toContain('Invalid transition');
    expect(code).toContain('const validTransitions');
    // ...with the flow-derived map that forbids the undeclared draft→closed jump.
    expect(code).toContain('"draft":["open"]');
    expect(code).not.toContain('"draft":["open","closed"]');
  });

  // Phase 2 Slice 14 actor wiring — validate() ALWAYS accepts _actor (default
  // null) on EVERY controller, so route handlers can pass `(request as any).user`
  // without per-model knowledge of whether constraints exist. Unconstrained
  // controllers just ignore the param.
  it('emits _actor param on validate() signature for both constrained AND unconstrained', () => {
    const constrained = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildConstrainedModel(),
      controller: buildController('Vote'),
      models: [buildConstrainedModel()],
    });
    const unconstrained = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildUnconstrainedModel(),
      controller: buildController('Comment'),
      models: [buildUnconstrainedModel()],
    });
    expect(constrained).toContain('_actor: any = null');
    expect(unconstrained).toContain('_actor: any = null');
    // Unconstrained controllers still don't pull in the guards module.
    expect(unconstrained).not.toContain('runConstraintGuards');
  });

  it('emits delete() with validate call when constrained', () => {
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildConstrainedModel(),
      controller: buildController('Vote'),
      models: [buildConstrainedModel()],
    });
    // Slice 14 — delete() now takes (id, _actor) and threads _actor to validate.
    expect(code).toContain('public async delete(id: string, _actor: any = null)');
    expect(code).toContain(`await this.validate(vote, { operation: 'delete' }, _actor)`);
  });

  it('emits delete() WITHOUT validate call when unconstrained (backward compat)', () => {
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildUnconstrainedModel(),
      controller: buildController('Comment'),
      models: [buildUnconstrainedModel()],
    });
    // Slice 14 — delete still accepts _actor for uniform route handler calls.
    expect(code).toContain('public async delete(id: string, _actor: any = null)');
    // Original delete just does findUnique + delete + publish — no validate.
    const deleteSection = code.substring(code.indexOf('public async delete(id: string'));
    expect(deleteSection).not.toContain('this.validate(');
  });

  // Phase 2 carry-over (Update self-from-DB).
  it('update() loads + merges entity before validate when constrained', () => {
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildConstrainedModel(),
      controller: buildController('Vote'),
      models: [buildConstrainedModel()],
    });
    const updateSection = code.substring(code.indexOf('public async update(id: string'));
    expect(updateSection).toContain('__existing');
    expect(updateSection).toContain('findUnique');
    expect(updateSection).toContain('__merged');
    expect(updateSection).toContain(`await this.validate(__merged, { operation: 'update' }, _actor)`);
  });

  it('update() uses raw input shape when unconstrained (backward compat)', () => {
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildUnconstrainedModel(),
      controller: buildController('Comment'),
      models: [buildUnconstrainedModel()],
    });
    const updateSection = code.substring(code.indexOf('public async update(id: string'));
    expect(updateSection).not.toContain('__existing');
    expect(updateSection).not.toContain('__merged');
    expect(updateSection).toContain(`await this.validate(data, { operation: 'update' }, _actor)`);
  });

  // Phase 2 Slice 15 — Create-time relation loading. Mirrors the
  // Update self-from-DB pattern but for create: load each belongsTo
  // related entity from input FKs before calling validate, so guards
  // traversing self.<rel>.<field> see the loaded entity.
  it('create() loads belongsTo relations into self when constrained', () => {
    const constrainedWithRels = {
      ...buildConstrainedModel(),
      relationships: [
        { name: 'poll', type: 'belongsTo', target: 'Poll' },
        { name: 'voter', type: 'belongsTo', target: 'User' },
      ],
    };
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: constrainedWithRels,
      controller: buildController('Vote'),
      models: [constrainedWithRels],
    });
    const createSection = code.substring(code.indexOf('public async create(data: any'));
    expect(createSection).toContain('__loadedRels');
    expect(createSection).toContain('if (data.pollId)');
    expect(createSection).toContain('prisma.poll.findUnique');
    expect(createSection).toContain('if (data.voterId)');
    expect(createSection).toContain('prisma.user.findUnique');
    expect(createSection).toContain('__mergedSelf = { ...data, ...__loadedRels }');
    expect(createSection).toContain(`await this.validate(__mergedSelf, { operation: 'create' }, _actor)`);
  });

  it('create() skips relation loading when unconstrained (backward compat)', () => {
    const unconstrainedWithRels = {
      ...buildUnconstrainedModel(),
      relationships: [{ name: 'author', type: 'belongsTo', target: 'User' }],
    };
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: unconstrainedWithRels,
      controller: buildController('Comment'),
      models: [unconstrainedWithRels],
    });
    const createSection = code.substring(code.indexOf('public async create(data: any'));
    expect(createSection).not.toContain('__loadedRels');
    expect(createSection).not.toContain('__mergedSelf');
    expect(createSection).toContain(`await this.validate(data, { operation: 'create' }, _actor)`);
  });

  it('create() with constraints but no belongsTo skips the load block', () => {
    // hasConstraints but model has no relationships — still emits
    // simple validate(data, ...) since there's nothing to load.
    const code = generatePrismaController({
      spec: {} as any,
      factory: {} as any,
      model: buildConstrainedModel(), // no relationships
      controller: buildController('Vote'),
      models: [buildConstrainedModel()],
    });
    const createSection = code.substring(code.indexOf('public async create(data: any'));
    expect(createSection).not.toContain('__loadedRels');
    expect(createSection).toContain(`await this.validate(data, { operation: 'create' }, _actor)`);
  });
});
