/**
 * Regression tests for the PostgreSQL native (pg) controller generator.
 *
 * Mirrors the mongodb-native suite — the conventions are intentionally
 * the same shape so spec authors can swap factories without rewriting
 * .specly text. These tests pin shape, not full compilation:
 *
 *   - CURVED ops emit pg helper calls (insertOne / findOneByField /
 *     updateOneById / deleteOneById / findAll / query)
 *   - Lifecycle transitions decode flow shorthand and emit a transition map
 *   - Behaviour-derived actions colliding with CRUD names emit a warning
 *     and are dropped (shared with mongo)
 *   - Custom action steps run through `matchPgStep` (CRUD-shape steps
 *     map to inline helper calls, the rest delegate to aiBehaviors)
 */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import generatePgNativeController from '../controller-generator.js';

const baseModel = {
  name: 'Todo',
  attributes: {
    id: { name: 'id', type: 'UUID', required: true, auto: 'uuid4' },
    title: { name: 'title', type: 'String', required: true },
    completed: { name: 'completed', type: 'Boolean', default: false },
  },
};

const baseController = {
  name: 'TodoController',
  description: 'Controller for Todo',
  cured: { create: true, retrieve: true, update: true, evolve: true, delete: true },
};

describe('Postgres native — controller-generator', () => {
  it('emits all CURVED ops via pgClient helpers', () => {
    const out = generatePgNativeController({
      controller: baseController,
      model: baseModel,
    } as any);
    expect(out).toContain(`TABLE_NAME = 'todos'`);
    expect(out).toContain('insertOne');
    expect(out).toContain('findOneByField');
    expect(out).toContain('updateOneById');
    expect(out).toContain('deleteOneById');
    expect(out).toContain('findAll');
    expect(out).not.toContain('PrismaClient');
    expect(out).not.toContain('prisma.');
    expect(out).not.toContain('getCollection');
  });

  it('does NOT import ObjectId — pg uses string ids end-to-end', () => {
    const out = generatePgNativeController({
      controller: baseController,
      model: baseModel,
    } as any);
    expect(out).not.toContain('ObjectId');
    expect(out).not.toContain('mongodb');
  });

  it('emits lifecycle transition map for `flow:` shorthand', () => {
    const out = generatePgNativeController({
      controller: baseController,
      model: {
        ...baseModel,
        lifecycles: { status: { name: 'status', flow: 'pending -> done' } },
      },
    } as any);
    expect(out).toContain('public async evolve(');
    expect(out).toContain('"pending":["done"]');
    expect(out).toContain('Invalid transition');
  });

  it('emits a working multi-state transition map for explicit states', () => {
    const out = generatePgNativeController({
      controller: baseController,
      model: {
        ...baseModel,
        lifecycles: {
          status: {
            name: 'status',
            states: ['draft', 'open', 'closed'],
            transitions: { open: 'draft -> open', close: 'open -> closed' },
          },
        },
      },
    } as any);
    expect(out).toContain('"draft":["open"]');
    expect(out).toContain('"open":["closed"]');
  });

  it('uses model.storage.table override when present', () => {
    const out = generatePgNativeController({
      controller: baseController,
      model: { ...baseModel, storage: { table: 'custom_todos' } },
    } as any);
    expect(out).toContain(`TABLE_NAME = 'custom_todos'`);
    expect(out).toContain('TABLE_NAME');
  });

  it('omits CURVED ops not declared on the controller', () => {
    const out = generatePgNativeController({
      controller: { ...baseController, cured: { retrieve: true } },
      model: baseModel,
    } as any);
    expect(out).toContain('public async retrieve(');
    expect(out).not.toContain('public async create(');
    expect(out).not.toContain('public async update(');
    expect(out).not.toContain('public async delete(');
    expect(out).not.toContain('public async evolve(');
  });

  describe('behaviour-derived actions', () => {
    let warnSpy: ReturnType<typeof vi.spyOn>;
    beforeEach(() => {
      warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    });
    afterEach(() => {
      warnSpy.mockRestore();
    });

    it('warns + drops behaviour-derived actions colliding with CRUD names', () => {
      const out = generatePgNativeController({
        controller: {
          ...baseController,
          actions: {
            delete: {
              description: 'Soft-delete a todo',
              parameters: { id: 'String' },
              steps: ['Set deletedAt to now'],
            },
          },
        },
        model: baseModel,
      } as any);
      // The custom delete should NOT be emitted (collides with CRUD delete).
      // Generator emits exactly one `public async delete(` (the CRUD op).
      const deleteMatches = out.match(/public async delete\(/g) || [];
      expect(deleteMatches.length).toBe(1);
      expect(warnSpy).toHaveBeenCalledTimes(1);
      const warnMsg = String(warnSpy.mock.calls[0]?.[0] ?? '');
      expect(warnMsg).toContain('TodoController.delete');
      expect(warnMsg).toContain('collides');
    });

    it('runs steps through matchPgStep — CRUD-shape steps inline pg helpers', () => {
      const out = generatePgNativeController({
        controller: {
          ...baseController,
          actions: {
            archive: {
              description: 'Archive a todo by id',
              parameters: { id: 'String' },
              steps: [
                'Find Todo by id or fail with 404',
                'Update Todo completed to true',
                'Emit TodoArchived event',
              ],
            },
          },
        },
        model: baseModel,
      } as any);
      expect(out).toContain('public async archive(');
      // Step 1: find-by-field convention
      expect(out).toContain(`findOneByField('todos', 'id', args.id)`);
      // Step 2: update-field convention
      expect(out).toContain(`updateOneById('todos', todo.id, { completed: true })`);
      // Step 3: send-event convention
      expect(out).toContain(`eventBus.publish('TodoArchived'`);
    });

    it('falls back to AI-behaviors for unmatched steps', () => {
      const out = generatePgNativeController({
        controller: {
          ...baseController,
          actions: {
            score: {
              description: 'Compute a todo priority score',
              parameters: { id: 'String' },
              steps: [
                'Compute priority score from urgency and effort',
                'Return score',
              ],
            },
          },
        },
        model: baseModel,
      } as any);
      expect(out).toContain(`aiBehaviors.computePriorityScoreFromUrgencyAndEffort`);
      expect(out).toContain(`from '../behaviors/TodoController.ai.js'`);
    });
  });
});
