/**
 * Regression tests for the Prisma schema generator.
 *
 * Pins the three TODO #53 L1 emission fixes shipped 2026-05-10:
 *
 *   53A — manifest's `storage.database` capability mapping is honoured
 *         when emitting the datasource block. PostgreSQL15 produces
 *         `provider = "postgresql"`; MongoDB6 produces `"mongodb"`;
 *         absent storage falls back to "sqlite" for back-compat.
 *
 *   53B — `*At` field names auto-promote to Prisma `DateTime` even when
 *         the spec declares the source type as `String` / `Int`. Avoids
 *         the "@updatedAt requires DateTime" Prisma error. Non-timestamp
 *         names like `status` / `category` are left alone.
 *
 *   53C — attribute/relationship name collisions on a single model fail
 *         loudly with a useful realize-time error rather than silently
 *         producing an invalid schema.
 *
 * Tests assert substrings (or thrown-error properties) rather than exact
 * file equality so unrelated formatting tweaks don't cascade.
 */
import { describe, it, expect, vi } from 'vitest';
import generatePrismaSchema from '../schema-generator.js';

const simpleModel = {
  name: 'Todo',
  attributes: [
    { name: 'id', type: 'String', required: true },
    { name: 'title', type: 'String', required: true },
  ],
  relationships: [],
};

describe('prisma/schema-generator — 53A storage.database threading', () => {
  it('emits provider = "postgresql" + env-based url when PostgreSQL15 is threaded', () => {
    const storageFactory = {
      name: 'PostgreSQL15',
      technology: { database: 'postgresql' },
    };
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
      storageFactory,
    } as any);
    expect(out).toContain('provider = "postgresql"');
    expect(out).toContain('url      = env("DATABASE_URL")');
    expect(out).not.toContain('"file:./dev.db"');
  });

  it('emits provider = "mongodb" when MongoDB6 is threaded', () => {
    const storageFactory = {
      name: 'MongoDB6',
      technology: { database: 'mongodb' },
    };
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
      storageFactory,
    } as any);
    expect(out).toContain('provider = "mongodb"');
    expect(out).toContain('url      = env("DATABASE_URL")');
  });

  it('falls back to factory.name when technology.database is missing', () => {
    // Older factories or post-clone shapes might lose the technology block.
    // The name itself ("PostgreSQL15") still encodes the database, and
    // deriveProviderFromStorage searches it as a secondary signal.
    const storageFactory = { name: 'PostgreSQL15' };
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
      storageFactory,
    } as any);
    expect(out).toContain('provider = "postgresql"');
  });

  it('falls back to sqlite default when no storageFactory is threaded', () => {
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
    } as any);
    expect(out).toContain('provider = "sqlite"');
    expect(out).toContain('url      = "file:./dev.db"');
  });

  it('respects implType.configuration.orm.provider as legacy fallback', () => {
    // Direct-call sites that don't go through realizeAll can still
    // configure provider via the legacy implType path. Pin this so
    // the fix doesn't regress callers we don't control.
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
      implType: { configuration: { orm: { provider: 'mysql' } } },
    } as any);
    expect(out).toContain('provider = "mysql"');
  });

  it('threaded storageFactory takes precedence over implType', () => {
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
      storageFactory: { name: 'PostgreSQL15', technology: { database: 'postgresql' } },
      // Conflicting legacy hint — should be overridden.
      implType: { configuration: { orm: { provider: 'sqlite' } } },
    } as any);
    expect(out).toContain('provider = "postgresql"');
    expect(out).not.toContain('provider = "sqlite"');
  });
});

describe('prisma/schema-generator — #75 dev-environment provider precedence', () => {
  const pg = { name: 'PostgreSQL15', technology: { database: 'postgresql' } };

  it('uses the development environment provider (sqlite) over a postgres storage factory', () => {
    // Default-template shape: production-grade PostgreSQL15 storage factory,
    // but environments.development declares sqlite for zero-deps dev. The
    // schema must follow the dev environment so it matches the backend .env
    // (DATABASE_URL=file:./dev.db) and `npm run setup` works out of the box.
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
      storageFactory: pg,
      devDatabase: { provider: 'sqlite' },
    } as any);
    expect(out).toContain('provider = "sqlite"');
    expect(out).toContain('"file:./dev.db"');
    expect(out).not.toContain('provider = "postgresql"');
  });

  it('honors a postgres development-environment provider', () => {
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
      storageFactory: pg,
      devDatabase: { provider: 'postgresql' },
    } as any);
    expect(out).toContain('provider = "postgresql"');
    expect(out).toContain('url      = env("DATABASE_URL")');
  });

  it('falls back to the storage factory when no dev-environment override is declared', () => {
    // Back-compat: manifests without environments.development.database keep
    // using the storage factory (the existing 53A behaviour).
    const out = generatePrismaSchema({
      spec: { models: [simpleModel] },
      models: [simpleModel],
      storageFactory: pg,
      devDatabase: undefined,
    } as any);
    expect(out).toContain('provider = "postgresql"');
  });
});

describe('prisma/schema-generator — 53B *At field DateTime promotion', () => {
  it('emits createdAt as DateTime even when spec type is "String"', () => {
    const model = {
      name: 'Article',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'createdAt', type: 'String', required: true },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [model] },
      models: [model],
    } as any);
    // Field-line padding: 15 columns minus name length, then DateTime.
    // We assert the type without spaces so the test doesn't break on
    // formatting tweaks.
    expect(out).toMatch(/createdAt\s+DateTime/);
    expect(out).not.toMatch(/createdAt\s+String/);
  });

  it('emits updatedAt as DateTime + @updatedAt even when spec type is "String"', () => {
    const model = {
      name: 'Article',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'updatedAt', type: 'String', required: true },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [model] },
      models: [model],
    } as any);
    expect(out).toMatch(/updatedAt\s+DateTime/);
    expect(out).toContain('@updatedAt');
    // Critical: the @updatedAt annotation must NOT appear on a String field.
    expect(out).not.toMatch(/updatedAt\s+String[^\n]*@updatedAt/);
  });

  it('promotes other *At fields too: publishedAt, expiresAt, deletedAt', () => {
    const model = {
      name: 'Post',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'publishedAt', type: 'String' },
        { name: 'expiresAt', type: 'String' },
        { name: 'deletedAt', type: 'String' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [model] },
      models: [model],
    } as any);
    expect(out).toMatch(/publishedAt\s+DateTime/);
    expect(out).toMatch(/expiresAt\s+DateTime/);
    expect(out).toMatch(/deletedAt\s+DateTime/);
  });

  it('keeps DateTime fields as DateTime (no-op promotion)', () => {
    const model = {
      name: 'Article',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'updatedAt', type: 'DateTime' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [model] },
      models: [model],
    } as any);
    expect(out).toMatch(/updatedAt\s+DateTime/);
  });

  it('promotes Int *At fields to DateTime (unix-millis source pattern)', () => {
    const model = {
      name: 'Article',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'createdAt', type: 'Int' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [model] },
      models: [model],
    } as any);
    expect(out).toMatch(/createdAt\s+DateTime/);
  });

  it('does NOT promote non-*At String fields (status / category guard)', () => {
    const model = {
      name: 'Article',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'status', type: 'String' },
        { name: 'category', type: 'String' },
        { name: 'description', type: 'String' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [model] },
      models: [model],
    } as any);
    expect(out).toMatch(/status\s+String/);
    expect(out).toMatch(/category\s+String/);
    expect(out).toMatch(/description\s+String/);
    expect(out).not.toMatch(/status\s+DateTime/);
    expect(out).not.toMatch(/category\s+DateTime/);
  });

  it('does NOT promote names ending in lowercase "at" without uppercase A', () => {
    // The pattern requires a capital A: bat / cat / hat / chat / format
    // are common false-positives that don't match isTimestampFieldName.
    const model = {
      name: 'Thing',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'format', type: 'String' },     // ends in "at" lowercase
        { name: 'chat', type: 'String' },       // common-noun
        { name: 'flat', type: 'String' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [model] },
      models: [model],
    } as any);
    expect(out).toMatch(/format\s+String/);
    expect(out).toMatch(/chat\s+String/);
    expect(out).toMatch(/flat\s+String/);
    expect(out).not.toContain('DateTime');
  });

  it('does NOT promote a literal-suffix "Bat" that ends in capital "At"-like sequence', () => {
    // Edge case: someone names a field `wombat` (lowercase) — fine.
    // But what about `Habitat`? Pascal-case (caps lead) we explicitly
    // exclude via the leading-lowercase regex anchor.
    const model = {
      name: 'Animal',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'wombat', type: 'String' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [model] },
      models: [model],
    } as any);
    // wombat ends in lowercase "at", not "At" — must NOT promote.
    expect(out).toMatch(/wombat\s+String/);
  });
});

describe('prisma/schema-generator — 53C attribute/relationship collision', () => {
  // Prior behaviour: the first collision threw, killing emission of every
  // subsequent model. New behaviour: rename the colliding attribute to
  // `<name>Data` (the historical error message's own suggestion), emit a
  // console.warn, and proceed so the rest of the spec still produces a
  // Prisma schema. The auto-rename only happens inside the schema
  // generator — downstream services that key off the original attribute
  // name will need spec-level disambiguation if they reference the
  // renamed field; the goal here is "realize produces a usable schema",
  // not "spec is healed end-to-end".
  it('auto-renames the colliding attribute and still emits the model + relationship', () => {
    const saveGameModel = {
      name: 'SaveGame',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'gameState', type: 'String' },
      ],
      relationships: [
        { name: 'gameState', type: 'belongsTo', target: 'GameState' },
      ],
    };
    const gameStateModel = {
      name: 'GameState',
      attributes: [{ name: 'id', type: 'String', required: true }],
      relationships: [],
    };
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    let out = '';
    let messages = '';
    try {
      out = generatePrismaSchema({
        spec: { models: [saveGameModel, gameStateModel] },
        models: [saveGameModel, gameStateModel],
      } as any);
      // Capture warn.mock.calls BEFORE mockRestore — restoring resets state.
      messages = warn.mock.calls.map((c) => c.join(' ')).join('\n');
    } finally {
      warn.mockRestore();
    }
    // Original String attribute is now named gameStateData
    expect(out).toMatch(/gameStateData\s+String/);
    // belongsTo relationship keeps the canonical `gameState` name
    expect(out).toMatch(/gameState\s+GameState\b/);
    // SaveGame still emits, GameState still emits
    expect(out).toMatch(/model SaveGame \{/);
    expect(out).toMatch(/model GameState \{/);
    // Warning emitted naming the model + the rename
    expect(messages).toContain('SaveGame');
    expect(messages).toContain('gameState');
    expect(messages).toContain('gameStateData');
  });

  it('renames against default-derived relationship names too (no explicit rel.name)', () => {
    // Default name for a hasMany relation = pluralised target lowercase.
    // The attribute walker can produce a field that shadows that default.
    const userModel = {
      name: 'User',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'posts', type: 'String' }, // shadows default rel name "posts"
      ],
      relationships: [
        // No `name:` — generator derives "posts" from pluralize("post")
        { type: 'hasMany', target: 'Post' },
      ],
    };
    const post = {
      name: 'Post',
      attributes: [{ name: 'id', type: 'String', required: true }],
      relationships: [{ type: 'belongsTo', target: 'User' }],
    };
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    let out: string;
    try {
      out = generatePrismaSchema({
        spec: { models: [userModel, post] },
        models: [userModel, post],
      } as any);
    } finally {
      warn.mockRestore();
    }
    expect(out).toMatch(/postsData\s+String/);
  });

  it('proceeds normally when no collision exists (no false positives, no warnings)', () => {
    const model = {
      name: 'User',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'name', type: 'String' },
        { name: 'email', type: 'String' },
      ],
      relationships: [
        { name: 'posts', type: 'hasMany', target: 'Post' },
      ],
    };
    const post = {
      name: 'Post',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'title', type: 'String' },
      ],
      relationships: [
        { name: 'author', type: 'belongsTo', target: 'User' },
      ],
    };
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    let callCount = 0;
    try {
      expect(() =>
        generatePrismaSchema({
          spec: { models: [model, post] },
          models: [model, post],
        } as any)
      ).not.toThrow();
      callCount = warn.mock.calls.length;
    } finally {
      warn.mockRestore();
    }
    expect(callCount).toBe(0);
  });

  it('renames multiple colliding attributes in a single model independently', () => {
    const model = {
      name: 'WideModel',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'foo', type: 'String' },
        { name: 'bar', type: 'String' },
      ],
      relationships: [
        { name: 'foo', type: 'belongsTo', target: 'Foo' },
        { name: 'bar', type: 'hasOne', target: 'Bar' },
      ],
    };
    const foo = { name: 'Foo', attributes: [{ name: 'id', type: 'String' }], relationships: [] };
    const bar = { name: 'Bar', attributes: [{ name: 'id', type: 'String' }], relationships: [] };
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    let out: string;
    try {
      out = generatePrismaSchema({
        spec: { models: [model, foo, bar] },
        models: [model, foo, bar],
      } as any);
    } finally {
      warn.mockRestore();
    }
    expect(out).toMatch(/fooData\s+String/);
    expect(out).toMatch(/barData\s+String/);
  });

  it('blast-radius regression: a collision in one model does NOT prevent later models from emitting', () => {
    // Pre-fix this site threw on the first collision, killing emission of
    // every subsequent model in the loop. Pin the new contract: 24 models
    // with one offender still produces all 24 in the output.
    const offender = {
      name: 'SaveGame',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'gameState', type: 'String' },
      ],
      relationships: [
        { name: 'gameState', type: 'belongsTo', target: 'GameState' },
      ],
    };
    const innocent1 = {
      name: 'Player',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'name', type: 'String' },
      ],
      relationships: [],
    };
    const innocent2 = {
      name: 'GameState',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'phase', type: 'String' },
      ],
      relationships: [],
    };
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    let out: string;
    try {
      out = generatePrismaSchema({
        spec: { models: [offender, innocent1, innocent2] },
        models: [offender, innocent1, innocent2],
      } as any);
    } finally {
      warn.mockRestore();
    }
    expect(out).toMatch(/model SaveGame \{/);
    expect(out).toMatch(/model Player \{/);
    expect(out).toMatch(/model GameState \{/);
  });
});

describe('prisma/schema-generator — 43B composite primary key', () => {
  // model.keys: [a, b] (or model.primaryKey: [a, b]) → @@id([a, b]) at
  // the model level. Mirrors postgres-native's parseColumnList contract
  // so the same spec field works across both adapters.
  it('emits @@id([a, b]) when model.keys declares a composite PK', () => {
    const enrolment = {
      name: 'Enrolment',
      keys: ['studentId', 'courseId'],
      attributes: [
        { name: 'studentId', type: 'String', required: true },
        { name: 'courseId', type: 'String', required: true },
        { name: 'enrolledAt', type: 'DateTime' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [enrolment] },
      models: [enrolment],
    } as any);
    expect(out).toMatch(/@@id\(\[studentId, courseId\]\)/);
  });

  it('also accepts model.primaryKey as an alias for model.keys', () => {
    const m = {
      name: 'Pair',
      primaryKey: ['a', 'b'],
      attributes: [
        { name: 'a', type: 'String', required: true },
        { name: 'b', type: 'String', required: true },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toMatch(/@@id\(\[a, b\]\)/);
  });

  it('does NOT emit @id on the lone id column when composite PK is declared', () => {
    // Without suppression, an `id` attribute would carry column-level @id;
    // Prisma rejects having both column-level @id AND model-level @@id.
    const m = {
      name: 'Compound',
      keys: ['a', 'b'],
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'a', type: 'String', required: true },
        { name: 'b', type: 'String', required: true },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    const block = out.match(/model Compound \{[\s\S]*?\n\}/)?.[0] ?? '';
    expect(block).toMatch(/@@id\(\[a, b\]\)/);
    expect(block).not.toMatch(/^\s+id\s+\S+\s+@id/m);
  });

  it('does NOT emit @@id when keys is a single-column array (degenerate)', () => {
    // [a] is degenerate — column-level @id on `a` is the right shape.
    const m = {
      name: 'Single',
      keys: ['a'],
      attributes: [{ name: 'a', type: 'String', required: true }],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).not.toMatch(/@@id\(/);
  });
});

describe('prisma/schema-generator — 43D composite uniques + partial indexes', () => {
  it('emits @@unique([a, b]) for one composite-unique declaration', () => {
    // model.unique: ['a', 'b'] → single composite spanning a and b.
    const m = {
      name: 'Uniq',
      unique: ['a', 'b'],
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'a', type: 'String', required: true },
        { name: 'b', type: 'String', required: true },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toMatch(/@@unique\(\[a, b\]\)/);
  });

  it('emits multiple @@unique blocks when model.unique is array-of-arrays', () => {
    const m = {
      name: 'Multi',
      unique: [['a', 'b'], ['c', 'd']],
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'a', type: 'String' },
        { name: 'b', type: 'String' },
        { name: 'c', type: 'String' },
        { name: 'd', type: 'String' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toMatch(/@@unique\(\[a, b\]\)/);
    expect(out).toMatch(/@@unique\(\[c, d\]\)/);
  });

  it('emits @@index([col], where: "...") for partial index', () => {
    // attr.index: { where: "deletedAt IS NULL" } → @@index with where clause.
    const m = {
      name: 'Active',
      attributes: [
        { name: 'id', type: 'String', required: true },
        {
          name: 'email',
          type: 'String',
          index: { where: 'deletedAt IS NULL' },
        },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toMatch(/@@index\(\[email\], where: "deletedAt IS NULL"\)/);
  });

  it('emits @@unique (not @@index) for partial-unique attr.index.unique=true', () => {
    const m = {
      name: 'ActiveUniq',
      attributes: [
        { name: 'id', type: 'String', required: true },
        {
          name: 'email',
          type: 'String',
          index: { where: 'deletedAt IS NULL', unique: true },
        },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toMatch(/@@unique\(\[email\], where: "deletedAt IS NULL"\)/);
    expect(out).not.toMatch(/@@index\(\[email\], where:/);
  });

  it('escapes embedded quotes in where-clause to keep emitted string valid', () => {
    const m = {
      name: 'Quoted',
      attributes: [
        { name: 'id', type: 'String', required: true },
        {
          name: 'tag',
          type: 'String',
          index: { where: 'tag != "deleted"' },
        },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toContain('where: "tag != \\"deleted\\""');
  });
});

describe('prisma/schema-generator — 43C JSON field type pass-through', () => {
  // The prisma side already handles Json correctly (mapTypeToPrisma's
  // typeLower.includes('json') branch). These tests pin that the
  // Json type emits Prisma's `Json` keyword unchanged, with no spurious
  // `String` fallback. The validator/UI generator side of #43C is a
  // separate scope tracked for follow-up.
  it('emits Prisma Json for spec attribute type "Json"', () => {
    const m = {
      name: 'Doc',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'metadata', type: 'Json' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toMatch(/metadata\s+Json/);
    expect(out).not.toMatch(/metadata\s+String/);
  });

  it('emits Prisma Json for spec attribute type "JSON" (case-insensitive)', () => {
    const m = {
      name: 'Doc',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'payload', type: 'JSON' },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toMatch(/payload\s+Json/);
  });

  it('emits Prisma Json when dbMapping.columnType is JSONB', () => {
    const m = {
      name: 'Doc',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'payload', type: 'String', dbMapping: { columnType: 'JSONB' } },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({ spec: { models: [m] }, models: [m] } as any);
    expect(out).toMatch(/payload\s+Json/);
  });
});

describe('prisma/schema-generator — 33 dual-declaration (attribute + lifecycle on same field)', () => {
  // When a model declares a field as BOTH an attribute (`status: String`)
  // AND a lifecycle (`lifecycles.status.flow: ...`), pre-#33 the generator
  // double-emitted — the field landed twice in the Prisma schema, which
  // Prisma rejects with `Field "status" is already defined on model "X"`.
  //
  // The current generator dedupes at line 475 of schema-generator.ts:
  // when `attributes.some(a => a.name === fieldName)` is true, the
  // lifecycle emission is skipped entirely. The attribute wins; the
  // lifecycle's state enum is dropped.
  //
  // These tests pin that dedup so a future change can't silently regress
  // to the double-emit shape that idle-meta hit on 2026-04-29.

  it('does NOT double-emit when attribute and lifecycle share a name (status)', () => {
    const job = {
      name: 'Job',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'title', type: 'String', required: true },
        { name: 'status', type: 'String' },
      ],
      lifecycles: [
        { name: 'status', states: ['draft', 'published', 'archived'] },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [job] },
      models: [job],
    } as any);
    // Find the Job model block and count `status` field declarations.
    const jobBlock = out.match(/model Job \{[\s\S]*?\n\}/)?.[0] ?? '';
    const statusFieldDecls = jobBlock.match(/^\s+status\s+/gm) ?? [];
    expect(statusFieldDecls.length).toBe(1);
  });

  it('attribute wins over lifecycle when both declare the same field', () => {
    // Attribute is plain String; lifecycle would have provided an enum.
    // Current behaviour: attribute wins (preserves the type the user
    // declared explicitly). Documented as the precedence — changing to
    // "lifecycle wins" would be a future migration.
    const job = {
      name: 'Job',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'status', type: 'String' },
      ],
      lifecycles: [
        { name: 'status', states: ['draft', 'open'] },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [job] },
      models: [job],
    } as any);
    const jobBlock = out.match(/model Job \{[\s\S]*?\n\}/)?.[0] ?? '';
    // The status field carries the attribute's String type, NOT the
    // lifecycle-derived JobStatus enum.
    expect(jobBlock).toMatch(/status\s+String/);
    expect(jobBlock).not.toMatch(/status\s+JobStatus/);
  });

  it('lifecycle is emitted as enum when no attribute conflicts (control case)', () => {
    // Sanity: when there's NO attribute on the same name, the lifecycle's
    // enum DOES land in the schema. Confirms the dedup is name-scoped,
    // not blanket-disabling lifecycle emission.
    const job = {
      name: 'Job',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'title', type: 'String', required: true },
      ],
      lifecycles: [
        { name: 'status', states: ['draft', 'open', 'closed'] },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [job] },
      models: [job],
    } as any);
    expect(out).toMatch(/status\s+JobStatus\s+@default\(draft\)/);
    expect(out).toMatch(/enum JobStatus\s*\{/);
  });

  it('produces valid Prisma — no duplicate field declaration error possible', () => {
    // The whole point: pre-#33 this combo produced invalid Prisma. Now
    // it produces exactly one `status` line. Use a regex that would
    // catch the bug shape: two `status\s+` lines on consecutive lines
    // inside the same model block.
    const job = {
      name: 'Job',
      attributes: [
        { name: 'id', type: 'String', required: true },
        { name: 'status', type: 'String' },
      ],
      lifecycles: [
        { name: 'status', states: ['draft', 'open'] },
      ],
      relationships: [],
    };
    const out = generatePrismaSchema({
      spec: { models: [job] },
      models: [job],
    } as any);
    expect(out).not.toMatch(/^\s+status\s+\S+.*\n\s+status\s+/m);
  });
});
