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 exactly the 3 files and NO relationship config when relations is empty and no tenant', () => {
    const noTenant = fixture({ tenantMode: 'none' })
    const files = generate(noTenant)
    expect(files).toHaveLength(3)
    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(noTenant)
    expect(cfg).not.toContain('HasOne')
    expect(cfg).not.toContain('HasForeignKey')
    expect(entityOf(noTenant)).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', targetScope: 'core' }),
      ],
    }))
    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)')
  })
})

describe('scaffold-entity / generate — Tenant FK (SmartStackExtensionDbContext-aligned, v3.55+)', () => {
  it('strict tenant → real FK to core.tenant_Tenants via the base-class Tenant DbSet (no local stub)', () => {
    const i = fixture({ tenantMode: 'strict' })
    const cfg = configOf(i)
    // Base-class Tenant — no TenantReference stub, no local External/ file.
    expect(cfg).toContain('builder.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId).OnDelete(DeleteBehavior.Restrict);')
    expect(cfg).not.toContain('TenantReference')
    expect(generate(i).some(f => f.path.includes('Configurations/External/'))).toBe(false)
    // The Tenant namespace is imported (both entity and config).
    expect(cfg).toContain('using SmartStack.Domain.Platform.Administration.Tenants;')
    expect(entityOf(i)).toContain('using SmartStack.Domain.Platform.Administration.Tenants;')
    expect(entityOf(i)).toContain('public Guid TenantId { get; private set; }')
  })

  it('optional tenant → still a real FK, nullable column, base-class Tenant', () => {
    const i = fixture({ tenantMode: 'optional' })
    expect(configOf(i)).toContain('builder.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId)')
    expect(entityOf(i)).toContain('public Guid? TenantId { get; private set; }')
    expect(generate(i).some(f => f.path.includes('Configurations/External/'))).toBe(false)
  })

  it('tenantMode none → no Tenant FK and no External stub file', () => {
    const i = fixture({ tenantMode: 'none' })
    expect(configOf(i)).not.toContain('Tenant>')
    expect(configOf(i)).not.toContain('TenantId')
    expect(generate(i).some(f => f.path.includes('Configurations/External/'))).toBe(false)
  })
})

describe('scaffold-entity / generate — core whitelist + cross-module references (non-negotiable FK)', () => {
  it('core whitelist ref (Department) → real cross-schema FK + nav property, NO stub file', () => {
    const i = fixture({
      tenantMode: 'none',
      relations: [rel({ type: 'many-to-one', targetEntity: 'Department', foreignKey: 'DepartmentId', nullable: true, targetScope: 'core' })],
    })
    const cfg = configOf(i)
    const entity = entityOf(i)
    expect(entity).toContain('public Guid? DepartmentId { get; private set; }')
    // Real nav property to the Core principal (resolved against the base class).
    expect(entity).toContain('public Department? Department { get; private set; }')
    expect(cfg).toContain('builder.HasOne(e => e.Department).WithMany().HasForeignKey(e => e.DepartmentId).OnDelete(DeleteBehavior.Restrict);')
    // No local stub — base class owns the ExcludeFromMigrations mapping.
    expect(generate(i).some(f => f.path.includes('Configurations/External/'))).toBe(false)
    // Department namespace imported in both files.
    expect(entity).toContain('using SmartStack.Domain.Platform.Administration.References;')
    expect(cfg).toContain('using SmartStack.Domain.Platform.Administration.References;')
  })

  it('core whitelist ref (User) → required FK + non-null nav + factory parameter', () => {
    const i = fixture({
      tenantMode: 'none',
      relations: [rel({ type: 'many-to-one', targetEntity: 'User', foreignKey: 'CustomerUserId', targetScope: 'core' })],
    })
    const entity = entityOf(i)
    expect(entity).toContain('public Guid CustomerUserId { get; private set; }')
    expect(entity).toContain('public User User { get; private set; } = null!;')
    expect(entity).toMatch(/public static Opportunity Create\([^)]*Guid customerUserId[^)]*\)/)
    expect(entity).toContain('using SmartStack.Domain.Platform.Administration.Users;')
  })

  it('cross-module ref → FK to the REAL type, NO navigation, NO stub', () => {
    const i = fixture({
      tenantMode: 'none',
      relations: [rel({ type: 'many-to-one', targetEntity: 'Contact', foreignKey: 'ContactId', targetScope: 'cross-module' })],
    })
    const cfg = configOf(i)
    const entity = entityOf(i)
    expect(entity).toContain('public Guid ContactId { get; private set; }')
    expect(entity).not.toContain('public Contact Contact')
    expect(cfg).toContain('builder.HasOne<Contact>().WithMany().HasForeignKey(e => e.ContactId).OnDelete(DeleteBehavior.Restrict);')
    expect(generate(i).some(f => f.path.includes('Configurations/External/'))).toBe(false)
  })

  it('same-module ref is unchanged — FK + navigation property (regression)', () => {
    const i = fixture({ tenantMode: 'none', relations: [rel({ type: 'many-to-one', targetEntity: 'Stage', foreignKey: 'StageId' })] })
    expect(entityOf(i)).toContain('public Stage Stage { get; private set; } = null!;')
    expect(configOf(i)).toContain('builder.HasOne(e => e.Stage).WithMany().HasForeignKey(e => e.StageId)')
  })

  it('an allowlisted audit column (CreatedByUserId) stays a plain Guid — no FK, no relation', () => {
    const i = fixture({ tenantMode: 'none', fields: [field('Title'), field('CreatedByUserId', { type: 'guid', required: false })] })
    const cfg = configOf(i)
    expect(entityOf(i)).toContain('public Guid? CreatedByUserId { get; private set; }')
    expect(cfg).not.toContain('CreatedByUserId')
  })
})

describe('scaffold-entity / validate — scope guards (V1 whitelist)', () => {
  it('rejects a core relation to a non-whitelist entity (UserSession) with whitelist guidance', () => {
    const res = validate(fixture({ relations: [rel({ type: 'many-to-one', targetEntity: 'UserSession', targetScope: 'core' })] }))
    expect(res.valid).toBe(false)
    expect(res.errors.join(' ')).toContain('whitelist')
    expect(res.errors.join(' ')).toContain('UserSession')
    expect(res.errors.join(' ')).toContain('ICoreDataService')
  })

  it('rejects a non-same-module scope on a collection relation', () => {
    const res = validate(fixture({ relations: [rel({ type: 'one-to-many', targetEntity: 'Invoice', targetScope: 'cross-module' })] }))
    expect(res.valid).toBe(false)
    expect(res.errors.join(' ')).toContain('only valid on a many-to-one')
  })

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

  it('accepts every V1 whitelist entity (User, Role, Tenant, Company, Department, JobTitle, Office, Language, Group)', () => {
    const whitelist = ['User', 'Role', 'Tenant', 'Company', 'Department', 'JobTitle', 'Office', 'Language', 'Group']
    for (const target of whitelist) {
      // Tenant's default FK column (TenantId) is reserved for the auto-emitted
      // tenant FK (from tenantMode), so a manual Tenant relation must use a
      // distinct FK name — Tenant is still whitelisted and must be accepted.
      const foreignKey = target === 'Tenant' ? 'OwnerTenantId' : `${target}Id`
      const res = validate(fixture({
        relations: [rel({ type: 'many-to-one', targetEntity: target, foreignKey, nullable: true, targetScope: 'core' })],
      }))
      expect(res.valid, `${target} should be accepted`).toBe(true)
    }
  })
})
