import { describe, it, expect } from 'vitest'
import { generate } from '../generate.js'
import { validate } from '../validate.js'
import type { ScaffoldEntityInput, EntityField, EntityRelation } from '../types.js'

function field(name: string, over: Partial<EntityField> = {}): EntityField {
  return { name, type: 'string', required: true, isKey: false, indexed: false, ...over }
}

function rel(over: Partial<EntityRelation> & Pick<EntityRelation, 'type' | 'targetEntity'>): EntityRelation {
  return { nullable: false, cascadeDelete: false, ...over }
}

function fixture(overrides: Partial<ScaffoldEntityInput> = {}): ScaffoldEntityInput {
  return {
    name: 'Opportunity',
    pluralName: 'Opportunities',
    module: 'pipeline',
    appCode: 'TestV2',
    domainPrefix: 'pipeline',
    namespace: 'TestV2',
    tenantMode: 'strict',
    schemaTarget: 'extensions',
    fields: [field('Title')],
    relations: [],
    constraints: [],
    projectPath: '/tmp/project',
    ...overrides,
  }
}

const entityOf = (i: ScaffoldEntityInput) => generate(i).find(f => f.path.endsWith('Opportunity.cs'))!.content
const configOf = (i: ScaffoldEntityInput) => generate(i).find(f => f.path.endsWith('OpportunityConfiguration.cs'))!.content

describe('scaffold-entity / generate — no relations (regression guard)', () => {
  it('emits the 3 files and NO relationship config when relations is empty', () => {
    const files = generate(fixture())
    expect(files.map(f => f.path)).toEqual(expect.arrayContaining([
      expect.stringContaining('Domain/Entities/Opportunity.cs'),
      expect.stringContaining('Domain/Entities/OpportunityEvents.cs'),
      expect.stringContaining('Infrastructure/Persistence/Configurations/OpportunityConfiguration.cs'),
    ]))
    const cfg = configOf(fixture())
    expect(cfg).not.toContain('HasOne')
    expect(cfg).not.toContain('HasForeignKey')
    expect(entityOf(fixture())).not.toContain('ICollection<')
  })
})

describe('scaffold-entity / generate — owning relation (many-to-one)', () => {
  it('emits the FK column, the reference navigation, and a real FK constraint (default Restrict)', () => {
    const i = fixture({ relations: [rel({ type: 'many-to-one', targetEntity: 'Contact' })] })
    const entity = entityOf(i)
    const config = configOf(i)
    expect(entity).toContain('public Guid ContactId { get; private set; }')
    expect(entity).toContain('public Contact Contact { get; private set; } = null!;')
    expect(config).toContain('builder.HasOne(e => e.Contact).WithMany().HasForeignKey(e => e.ContactId).OnDelete(DeleteBehavior.Restrict);')
    // required FK becomes a Create() parameter
    expect(entity).toMatch(/public static Opportunity Create\([^)]*Guid contactId[^)]*\)/)
  })

  it('maps cascade vocabulary to DeleteBehavior (cascade → Cascade)', () => {
    const i = fixture({ relations: [rel({ type: 'many-to-one', targetEntity: 'Account', onDelete: 'cascade' })] })
    expect(configOf(i)).toContain('.OnDelete(DeleteBehavior.Cascade);')
  })

  it('honors the legacy cascadeDelete boolean when onDelete is absent', () => {
    const i = fixture({ relations: [rel({ type: 'many-to-one', targetEntity: 'Account', cascadeDelete: true })] })
    expect(configOf(i)).toContain('.OnDelete(DeleteBehavior.Cascade);')
  })

  it('a nullable set-null relation yields a nullable FK + nullable nav + SetNull', () => {
    const i = fixture({ relations: [rel({ type: 'many-to-one', targetEntity: 'Owner', foreignKey: 'OwnerUserId', nullable: true, onDelete: 'set-null' })] })
    const entity = entityOf(i)
    expect(entity).toContain('public Guid? OwnerUserId { get; private set; }')
    expect(entity).toContain('public Owner? Owner { get; private set; }')
    expect(configOf(i)).toContain('builder.HasOne(e => e.Owner).WithMany().HasForeignKey(e => e.OwnerUserId).OnDelete(DeleteBehavior.SetNull);')
    // optional FK is NOT injected into the Create() initializer
    expect(entity).not.toContain('OwnerUserId = ownerUserId,')
  })

  it('reuses an existing hand-declared Guid field instead of emitting a duplicate', () => {
    const i = fixture({
      fields: [field('Title'), field('ContactId', { type: 'guid', required: false })],
      relations: [rel({ type: 'many-to-one', targetEntity: 'Contact', foreignKey: 'ContactId', nullable: true })],
    })
    const entity = entityOf(i)
    expect((entity.match(/public Guid\? ContactId \{ get; private set; \}/g) ?? []).length).toBe(1)
    expect(configOf(i)).toContain('.HasForeignKey(e => e.ContactId)')
  })
})

describe('scaffold-entity / generate — inverse relation (one-to-many)', () => {
  it('emits a collection navigation + HasMany config, and NO FK column on this side', () => {
    const i = fixture({ relations: [rel({ type: 'one-to-many', targetEntity: 'OrderLine', onDelete: 'cascade' })] })
    const entity = entityOf(i)
    expect(entity).toContain('public ICollection<OrderLine> OrderLines { get; private set; } = new List<OrderLine>();')
    expect(configOf(i)).toContain('builder.HasMany(e => e.OrderLines).WithOne().OnDelete(DeleteBehavior.Cascade);')
    expect(entity).not.toContain('OrderLineId')
  })
})

describe('scaffold-entity / validate — relation guards', () => {
  it('rejects set-null on a non-nullable relation', () => {
    const res = validate(fixture({ relations: [rel({ type: 'many-to-one', targetEntity: 'Owner', onDelete: 'set-null' })] }))
    expect(res.valid).toBe(false)
    expect(res.errors.join(' ')).toContain('set-null')
  })

  it('rejects two relations resolving to the same FK column', () => {
    const res = validate(fixture({
      relations: [
        rel({ type: 'many-to-one', targetEntity: 'Contact', foreignKey: 'PartyId' }),
        rel({ type: 'many-to-one', targetEntity: 'Company', foreignKey: 'PartyId' }),
      ],
    }))
    expect(res.valid).toBe(false)
    expect(res.errors.join(' ')).toContain('duplicate foreign key')
  })

  it('accepts a well-formed owning relation', () => {
    const res = validate(fixture({ relations: [rel({ type: 'many-to-one', targetEntity: 'Contact' })] }))
    expect(res.valid).toBe(true)
    expect(res.errors).toHaveLength(0)
  })
})

describe('scaffold-entity / generate — DB naming convention (SmartStack.app aligned)', () => {
  it('table = {domainPrefix}_{PascalPlural} in the target schema', () => {
    expect(configOf(fixture())).toContain('builder.ToTable("pipeline_Opportunities", SchemaConstants.Extensions);')
  })

  it('pluralizes the entity name when pluralName is omitted', () => {
    const i = fixture({ name: 'Company', pluralName: undefined })
    const cfg = generate(i).find(f => f.path.endsWith('CompanyConfiguration.cs'))!.content
    expect(cfg).toContain('builder.ToTable("pipeline_Companies", SchemaConstants.Extensions);')
  })

  it('never remaps columns — PascalCase default mapping, no HasColumnName', () => {
    const cfg = configOf(fixture({ fields: [field('FirstName', { maxLength: 100 })] }))
    expect(cfg).not.toContain('HasColumnName')
    expect(cfg).toContain('builder.Property(e => e.FirstName).IsRequired().HasMaxLength(100);')
  })

  it('emits no HasDatabaseName index overrides (EF default index names)', () => {
    expect(configOf(fixture({ fields: [field('Code', { indexed: true })] }))).not.toContain('HasDatabaseName')
  })

  it('a plain Guid FK gets no Property line and no column remap', () => {
    const cfg = configOf(fixture({ relations: [rel({ type: 'many-to-one', targetEntity: 'Contact' })] }))
    expect(cfg).not.toContain('HasColumnName')
    expect(cfg).not.toContain('builder.Property(e => e.ContactId)')
    expect(cfg).toContain('.HasForeignKey(e => e.ContactId)')
  })
})
