/**
 * Regression tests for the postgres-native DDL generator.
 *
 * Pins existing functionality (CREATE TABLE shape, type mapping, FK
 * indexes, lifecycle columns, default values, unique constraints) AND
 * the new Tier 1 / Tier 2 features added in engines 6.18.0:
 *   - Composite primary keys (`keys: [a, b]`, `primaryKey: [a, b]`)
 *   - Composite unique constraints (`unique: [[a, b]]`)
 *   - Partial indexes (`attr.index: { where, unique }`)
 *   - JSON type mapping (already present; pinned as a regression)
 *
 * Tests assert SQL substrings rather than full-text equality so a future
 * comment / formatting tweak doesn't cascade into 30 broken tests.
 */
import { describe, it, expect } from 'vitest';
import generatePgSchemaSql from '../ddl-generator.js';

const todoModel = {
  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 },
  },
};

describe('postgres-native — ddl-generator (existing functionality)', () => {
  it('emits CREATE TABLE IF NOT EXISTS + pgcrypto extension', () => {
    const sql = generatePgSchemaSql({ models: [todoModel] } as any);
    expect(sql).toContain('CREATE EXTENSION IF NOT EXISTS "pgcrypto"');
    expect(sql).toContain('CREATE TABLE IF NOT EXISTS "todos"');
  });

  it('maps types correctly (TEXT/INTEGER/BOOLEAN/TIMESTAMPTZ/UUID)', () => {
    const model = {
      name: 'Mix',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        title: { name: 'title', type: 'String', required: true },
        count: { name: 'count', type: 'Integer', required: true },
        ratio: { name: 'ratio', type: 'Float' },
        active: { name: 'active', type: 'Boolean', default: true },
        when: { name: 'when', type: 'DateTime' },
      },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('"title" TEXT NOT NULL');
    expect(sql).toContain('"count" INTEGER NOT NULL');
    expect(sql).toContain('"ratio" DOUBLE PRECISION');
    expect(sql).toContain('"active" BOOLEAN');
    expect(sql).toContain('"when" TIMESTAMPTZ');
    expect(sql).toContain('"id" UUID PRIMARY KEY DEFAULT gen_random_uuid()');
  });

  it('synthesises an id column when no id is declared', () => {
    const model = {
      name: 'NoId',
      attributes: { name: { name: 'name', type: 'String', required: true } },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text');
  });

  it('emits FK indexes for *Id columns', () => {
    const model = {
      name: 'Item',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        userId: { name: 'userId', type: 'UUID', required: true },
        categoryId: { name: 'categoryId', type: 'UUID', required: true },
      },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('CREATE INDEX IF NOT EXISTS "items_userId_idx" ON "items" ("userId")');
    expect(sql).toContain('CREATE INDEX IF NOT EXISTS "items_categoryId_idx" ON "items" ("categoryId")');
  });

  it('emits unique indexes for column-level `unique: true`', () => {
    const model = {
      name: 'User',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        email: { name: 'email', type: 'String', required: true, unique: true },
      },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('CREATE UNIQUE INDEX IF NOT EXISTS "users_email_uq" ON "users" ("email")');
  });

  it('honours `model.storage.table` override', () => {
    const sql = generatePgSchemaSql({
      models: [{ ...todoModel, storage: { table: 'custom_todos' } }],
    } as any);
    expect(sql).toContain('CREATE TABLE IF NOT EXISTS "custom_todos"');
    expect(sql).not.toContain('CREATE TABLE IF NOT EXISTS "todos"');
  });

  it('emits lifecycle columns auto-derived from `flow:` shorthand', () => {
    const model = {
      ...todoModel,
      lifecycles: { status: { name: 'status', flow: 'open -> closed' } },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain(`"status" TEXT NOT NULL DEFAULT 'open'`);
  });

  it('adds createdAt/updatedAt defaults if not declared', () => {
    const sql = generatePgSchemaSql({ models: [todoModel] } as any);
    expect(sql).toContain('"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()');
    expect(sql).toContain('"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()');
  });

  it('returns a stub when no models declared', () => {
    expect(generatePgSchemaSql({} as any)).toContain('No models declared');
    expect(generatePgSchemaSql({ models: [] } as any)).toContain('No models declared');
  });
});

describe('postgres-native — ddl-generator (engines 6.18.0 — Tier 1: composite keys + uniques)', () => {
  it('composite primary key via `keys: [a, b]` emits a table-level PRIMARY KEY constraint', () => {
    const model = {
      name: 'Membership',
      attributes: {
        tenantId: { name: 'tenantId', type: 'UUID', required: true },
        userId: { name: 'userId', type: 'UUID', required: true },
        role: { name: 'role', type: 'String', required: true },
      },
      keys: ['tenantId', 'userId'],
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    // Table-level constraint
    expect(sql).toContain('PRIMARY KEY ("tenantId", "userId")');
    // Both columns NOT NULL (forced by composite-PK membership)
    expect(sql).toContain('"tenantId" UUID NOT NULL');
    expect(sql).toContain('"userId" UUID NOT NULL');
    // No synthesised id column (composite PK takes its place)
    expect(sql).not.toContain('"id" TEXT PRIMARY KEY');
  });

  it('accepts `primaryKey:` as an alias for `keys:`', () => {
    const model = {
      name: 'Pair',
      attributes: {
        a: { name: 'a', type: 'String', required: true },
        b: { name: 'b', type: 'String', required: true },
      },
      primaryKey: ['a', 'b'],
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('PRIMARY KEY ("a", "b")');
  });

  it('composite unique constraint via `unique: [[a, b]]`', () => {
    const model = {
      name: 'Slug',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        tenantId: { name: 'tenantId', type: 'UUID', required: true },
        slug: { name: 'slug', type: 'String', required: true },
      },
      unique: [['tenantId', 'slug']],
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('UNIQUE ("tenantId", "slug")');
  });

  it('multiple composite uniques', () => {
    const model = {
      name: 'Multi',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        a: { name: 'a', type: 'String' },
        b: { name: 'b', type: 'String' },
        c: { name: 'c', type: 'String' },
      },
      unique: [['a', 'b'], ['b', 'c']],
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('UNIQUE ("a", "b")');
    expect(sql).toContain('UNIQUE ("b", "c")');
  });

  it('a single-array `unique: [a, b]` is treated as ONE composite (not two single-column)', () => {
    // This is the documented contract: single uniques go at the attribute
    // level, not in `model.unique`. So `unique: ["a", "b"]` is one
    // composite spanning a and b.
    const model = {
      name: 'Single',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        a: { name: 'a', type: 'String' },
        b: { name: 'b', type: 'String' },
      },
      unique: ['a', 'b'],
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('UNIQUE ("a", "b")');
    // Did NOT generate two separate single-column unique indexes.
    expect(sql).not.toContain('"single_a_uq"');
    expect(sql).not.toContain('"single_b_uq"');
  });

  it('does not double-emit FK indexes for columns that are part of composite PK', () => {
    // userId is the FK that would normally get an `_idx` index. When it's
    // part of the PK, the PK already covers index lookups starting on
    // userId, so the extra index would be wasted disk.
    const model = {
      name: 'Member',
      attributes: {
        userId: { name: 'userId', type: 'UUID', required: true },
        groupId: { name: 'groupId', type: 'UUID', required: true },
      },
      keys: ['userId', 'groupId'],
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).not.toContain('"members_userId_idx"');
    expect(sql).not.toContain('"members_groupId_idx"');
  });
});

describe('postgres-native — ddl-generator (engines 6.18.0 — Tier 2: partial indexes + JSON)', () => {
  it('partial unique index via `attr.index: { where, unique: true }`', () => {
    const model = {
      name: 'User',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        email: {
          name: 'email',
          type: 'String',
          required: true,
          // Soft-delete-aware unique: only enforce uniqueness for live rows.
          index: { where: '"deletedAt" IS NULL', unique: true },
        },
        deletedAt: { name: 'deletedAt', type: 'DateTime' },
      },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('CREATE UNIQUE INDEX IF NOT EXISTS "users_email_uq_partial" ON "users" ("email") WHERE "deletedAt" IS NULL');
  });

  it('plain partial index via `attr.index: { where: "..." }`', () => {
    const model = {
      name: 'Job',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        status: {
          name: 'status',
          type: 'String',
          index: { where: "status IN ('pending', 'running')" },
        },
      },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain(`CREATE INDEX IF NOT EXISTS "jobs_status_idx_partial" ON "jobs" ("status") WHERE status IN ('pending', 'running')`);
  });

  it('`attr.index: true` emits a plain index (no WHERE)', () => {
    const model = {
      name: 'Q',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        priority: { name: 'priority', type: 'Integer', index: true },
      },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    // Index emitted without WHERE clause.
    const idxLine = sql.match(/CREATE.*"qs_priority_idx_partial".*$/m)?.[0] ?? '';
    expect(idxLine).toContain('"priority"');
    expect(idxLine).not.toContain('WHERE');
  });

  it('JSON / Jsonb maps to JSONB column', () => {
    const model = {
      name: 'Doc',
      attributes: {
        id: { name: 'id', type: 'UUID', required: true },
        payload: { name: 'payload', type: 'Json', required: true },
        metadata: { name: 'metadata', type: 'Jsonb' },
      },
    };
    const sql = generatePgSchemaSql({ models: [model] } as any);
    expect(sql).toContain('"payload" JSONB NOT NULL');
    expect(sql).toContain('"metadata" JSONB');
  });
});
