import { describe, it, expect } from 'vitest'
import { generate } from '../generate.js'
import type { ScaffoldBusinessInput, BusinessCustomAction } from '../types.js'

function fixture(overrides: Partial<ScaffoldBusinessInput> = {}): ScaffoldBusinessInput {
  return {
    name: 'Budget',
    pluralName: 'Budgets',
    module: 'budgets',
    appCode: 'TestV2',
    namespace: 'TestV2',
    fields: [
      { name: 'Code', type: 'string', required: true, isKey: false },
      { name: 'Label', type: 'string', required: true, isKey: false },
      { name: 'InitialAmount', type: 'decimal', required: true, isKey: false },
    ],
    businessRules: [],
    customActions: [],
    projectPath: '/tmp/project',
    ...overrides,
  }
}

describe('scaffold-business / generate — standard CRUD', () => {
  it('emits Commands, Handlers, Service interface and impl for CRUD', () => {
    const files = generate(fixture())
    const paths = files.map((f) => f.path)
    expect(paths).toEqual(expect.arrayContaining([
      expect.stringContaining('Commands/CreateBudgetCommand.cs'),
      expect.stringContaining('Commands/UpdateBudgetCommand.cs'),
      expect.stringContaining('Commands/DeleteBudgetCommand.cs'),
      expect.stringContaining('Handlers/CreateBudgetCommandHandler.cs'),
      expect.stringContaining('Interfaces/IBudgetService.cs'),
      expect.stringContaining('Services/Budgets/BudgetService.cs'),
    ]))
  })

  it('service injects IExtensionsDbContext (not ICoreDbContext) — extension entities live in the extensions schema', () => {
    const files = generate(fixture())
    const svc = files.find((f) => f.path.endsWith('Services/Budgets/BudgetService.cs'))!
    expect(svc.content).toMatch(/private readonly IExtensionsDbContext _context;/)
    expect(svc.content).toMatch(/public BudgetService\(IExtensionsDbContext context\)/)
    expect(svc.content).not.toMatch(/ICoreDbContext/)
    // Set<T>() still drives the queries (exposed on IExtensionsDbContext).
    expect(svc.content).toMatch(/_context\.Set<Budget>\(\)/)
  })

  it('emits BOTH a Create and an Update CommandValidator', () => {
    const files = generate(fixture())
    const paths = files.map((f) => f.path)
    expect(paths).toEqual(expect.arrayContaining([
      expect.stringContaining('Validators/CreateBudgetCommandValidator.cs'),
      expect.stringContaining('Validators/UpdateBudgetCommandValidator.cs'),
    ]))
    const update = files.find((f) => f.path.endsWith('Validators/UpdateBudgetCommandValidator.cs'))!
    // Update validator guards the primary key and the required string fields.
    expect(update.content).toMatch(/RuleFor\(x => x\.Id\)\.NotEmpty\(\)/)
    expect(update.content).toMatch(/RuleFor\(x => x\.Label\)\.NotEmpty\(\)/)
  })

  it('applies each business rule to BOTH validators (invariant holds on update)', () => {
    const files = generate(fixture({
      businessRules: [
        {
          id: 'BR-001',
          description: 'Initial amount must be positive',
          expression: 'RuleFor(x => x.InitialAmount).GreaterThan(0)',
          validExamples: [],
          invalidExamples: [],
        },
      ],
    }))
    const create = files.find((f) => f.path.endsWith('Validators/CreateBudgetCommandValidator.cs'))!
    const update = files.find((f) => f.path.endsWith('Validators/UpdateBudgetCommandValidator.cs'))!
    expect(create.content).toMatch(/RuleFor\(x => x\.InitialAmount\)\.GreaterThan\(0\)/)
    expect(update.content).toMatch(/RuleFor\(x => x\.InitialAmount\)\.GreaterThan\(0\)/)
  })
})

describe('scaffold-business / generate — custom actions (per-page mode)', () => {
  function archive(): BusinessCustomAction {
    return {
      code: 'archive',
      scope: 'row',
      payloadDto: null,
      responseDto: 'NoContent',
    }
  }

  it('emits a Command record per custom action with row-scope param shape', () => {
    const files = generate(fixture({ customActions: [archive()] }))
    const cmd = files.find((f) => f.path.endsWith('ArchiveBudgetCommand.cs'))!
    expect(cmd).toBeDefined()
    expect(cmd.content).toMatch(/public record ArchiveBudgetCommand\(Guid Id\) : IRequest<Unit>;/)
  })

  it('emits a Handler that calls _service.{Code}Async and returns Unit for NoContent', () => {
    const files = generate(fixture({ customActions: [archive()] }))
    const handler = files.find((f) => f.path.endsWith('ArchiveBudgetCommandHandler.cs'))!
    expect(handler).toBeDefined()
    expect(handler.content).toMatch(/await _service\.ArchiveAsync\(request\.Id, cancellationToken\)/)
    expect(handler.content).toMatch(/return Unit\.Value;/)
  })

  it('adds the custom action to the I{Entity}Service interface', () => {
    const files = generate(fixture({ customActions: [archive()] }))
    const iface = files.find((f) => f.path.endsWith('IBudgetService.cs'))!
    expect(iface.content).toMatch(/Task ArchiveAsync\(Guid id, CancellationToken ct = default\);/)
  })

  it('emits a canonical recipe in the Service impl for "archive" code', () => {
    const files = generate(fixture({ customActions: [archive()] }))
    const svc = files.find((f) => f.path.includes('Infrastructure/Services/Budgets/BudgetService.cs'))!
    expect(svc.content).toMatch(/public async Task ArchiveAsync\(Guid id, CancellationToken/)
    expect(svc.content).toMatch(/var entity = await _context\.Set<Budget>\(\)\.FindAsync\(new object\[\] \{ id \}, ct\)/)
    // The canonical archive sets Status="archived"
    expect(svc.content).toMatch(/SetValue\(entity, "archived"\)/)
    expect(svc.content).toMatch(/await _context\.SaveChangesAsync\(ct\)/)
  })

  it('emits canonical restore / activate / deactivate recipes', () => {
    const files = generate(fixture({
      customActions: [
        { code: 'restore', scope: 'row', payloadDto: null, responseDto: 'NoContent' },
        { code: 'activate', scope: 'row', payloadDto: null, responseDto: 'NoContent' },
        { code: 'deactivate', scope: 'row', payloadDto: null, responseDto: 'NoContent' },
      ],
    }))
    const svc = files.find((f) => f.path.includes('Infrastructure/Services/Budgets/BudgetService.cs'))!
    expect(svc.content).toMatch(/SetValue\(entity, "active"\)/)
    expect(svc.content).toMatch(/SetValue\(entity, true\)/)
    expect(svc.content).toMatch(/SetValue\(entity, false\)/)
  })

  it('emits a canonical duplicate recipe (clone + new Guid + return clone)', () => {
    const files = generate(fixture({
      customActions: [
        { code: 'duplicate', scope: 'row', payloadDto: null, responseDto: 'BudgetDto' },
      ],
    }))
    const svc = files.find((f) => f.path.includes('Infrastructure/Services/Budgets/BudgetService.cs'))!
    expect(svc.content).toMatch(/public async Task<BudgetDto> DuplicateAsync\(Guid id/)
    expect(svc.content).toMatch(/var clone = \(Budget\)source\.GetType\(\)\.GetMethod\("Clone"\)/)
    expect(svc.content).toMatch(/SetValue\(clone, Guid\.NewGuid\(\)\)/)
    expect(svc.content).toMatch(/_context\.Set<Budget>\(\)\.Add\(clone\)/)
  })

  it('emits NotImplementedException stub for unknown codes with UC reference', () => {
    const files = generate(fixture({
      customActions: [
        { code: 'approve', scope: 'row', payloadDto: null, responseDto: 'NoContent', ucReference: 'UC-BUD-014' },
      ],
    }))
    const svc = files.find((f) => f.path.includes('Infrastructure/Services/Budgets/BudgetService.cs'))!
    expect(svc.content).toMatch(/public async Task ApproveAsync\(Guid id/)
    expect(svc.content).toMatch(/TODO\[UC-BUD-014\]: implement 'approve' per the linked use case\./)
    expect(svc.content).toMatch(/throw new NotImplementedException\(\);/)
  })

  it('handles bulk-scope custom action with IReadOnlyList<Guid> ids parameter', () => {
    const files = generate(fixture({
      customActions: [
        { code: 'archive', scope: 'bulk', payloadDto: null, responseDto: 'NoContent' },
      ],
    }))
    const cmd = files.find((f) => f.path.endsWith('ArchiveBudgetCommand.cs'))!
    expect(cmd.content).toMatch(/public record ArchiveBudgetCommand\(IReadOnlyList<Guid> Ids\)/)
    const iface = files.find((f) => f.path.endsWith('IBudgetService.cs'))!
    expect(iface.content).toMatch(/Task ArchiveAsync\(IReadOnlyList<Guid> ids, CancellationToken/)
  })

  it('handles row-scope action with payload DTO', () => {
    const files = generate(fixture({
      customActions: [
        { code: 'approve', scope: 'row', payloadDto: 'ApproveRequest', responseDto: 'BudgetDto' },
      ],
    }))
    const cmd = files.find((f) => f.path.endsWith('ApproveBudgetCommand.cs'))!
    expect(cmd.content).toMatch(/public record ApproveBudgetCommand\(Guid Id, ApproveRequest Payload\) : IRequest<BudgetDto>;/)
    const handler = files.find((f) => f.path.endsWith('ApproveBudgetCommandHandler.cs'))!
    expect(handler.content).toMatch(/return await _service\.ApproveAsync\(request\.Id, request\.Payload, cancellationToken\)/)
  })

  it('emits NO custom artefacts when customActions is empty (legacy default)', () => {
    const files = generate(fixture({ customActions: [] }))
    expect(files.find((f) => f.path.includes('Archive'))).toBeUndefined()
    expect(files.find((f) => f.path.includes('Duplicate'))).toBeUndefined()
  })

  it('pascalizes kebab-case codes for class and method names', () => {
    const files = generate(fixture({
      customActions: [
        { code: 'submit-for-review', scope: 'row', payloadDto: null, responseDto: 'NoContent' },
      ],
    }))
    const cmd = files.find((f) => f.path.endsWith('SubmitForReviewBudgetCommand.cs'))!
    expect(cmd).toBeDefined()
    expect(cmd.content).toMatch(/public record SubmitForReviewBudgetCommand\(Guid Id\)/)
    const handler = files.find((f) => f.path.endsWith('SubmitForReviewBudgetCommandHandler.cs'))!
    expect(handler.content).toMatch(/_service\.SubmitForReviewAsync\(request\.Id/)
  })
})

describe('scaffold-business / generate — lookup endpoint (feeds <EntityLookup>)', () => {
  it('emits {Entity}RefDto in the DTOs file', () => {
    const files = generate(fixture())
    const dtos = files.find((f) => f.path.endsWith('DTOs/BudgetDtos.cs'))!
    expect(dtos.content).toMatch(/public record BudgetRefDto\(Guid Id, string DisplayName\);/)
  })

  it('emits Get{Plural}LookupQuery alongside the standard queries', () => {
    const files = generate(fixture())
    const queries = files.find((f) => f.path.endsWith('Queries/GetBudgetQuery.cs'))!
    expect(queries.content).toMatch(
      /public record GetBudgetsLookupQuery\(string\? Search = null, int Page = 1, int PageSize = 20\) : IRequest<PaginatedResult<BudgetRefDto>>;/,
    )
  })

  it('emits Get{Plural}LookupQueryHandler that delegates to GetLookupAsync', () => {
    const files = generate(fixture())
    const handler = files.find((f) => f.path.endsWith('Handlers/GetBudgetsLookupQueryHandler.cs'))!
    expect(handler).toBeDefined()
    expect(handler.content).toMatch(
      /class GetBudgetsLookupQueryHandler : IRequestHandler<GetBudgetsLookupQuery, PaginatedResult<BudgetRefDto>>/,
    )
    expect(handler.content).toMatch(/=> _service\.GetLookupAsync\(request, cancellationToken\);/)
  })

  it('adds GetLookupAsync to the I{Entity}Service interface', () => {
    const files = generate(fixture())
    const iface = files.find((f) => f.path.endsWith('Interfaces/IBudgetService.cs'))!
    expect(iface.content).toMatch(
      /Task<PaginatedResult<BudgetRefDto>> GetLookupAsync\(GetBudgetsLookupQuery query, CancellationToken ct = default\);/,
    )
  })

  it('implements GetLookupAsync projecting the preferred display field (Label)', () => {
    // Fixture has Code, Label, InitialAmount — Label wins over Code per the
    // Name → Label → Code → Title priority order.
    const files = generate(fixture())
    const svc = files.find((f) => f.path.endsWith('Services/Budgets/BudgetService.cs'))!
    expect(svc.content).toMatch(/public async Task<PaginatedResult<BudgetRefDto>> GetLookupAsync/)
    expect(svc.content).toMatch(/EF\.Functions\.Like\(x\.Label, \$"%\{query\.Search\}%"\)/)
    expect(svc.content).toMatch(/\.OrderBy\(x => x\.Label\)/)
    expect(svc.content).toMatch(/new BudgetRefDto\(x\.Id, x\.Label\)/)
  })

  it('honours explicit displayNameExpr when it matches a stored field', () => {
    const files = generate(fixture({ displayNameExpr: 'Code' }))
    const svc = files.find((f) => f.path.endsWith('Services/Budgets/BudgetService.cs'))!
    expect(svc.content).toMatch(/new BudgetRefDto\(x\.Id, x\.Code\)/)
  })

  it('falls back to Id.ToString() when no string field is available', () => {
    const files = generate(fixture({
      fields: [
        { name: 'Amount', type: 'decimal', required: true, isKey: false },
        { name: 'IsActive', type: 'bool', required: true, isKey: false },
      ],
    }))
    const svc = files.find((f) => f.path.endsWith('Services/Budgets/BudgetService.cs'))!
    expect(svc.content).toMatch(/new BudgetRefDto\(x\.Id, x\.Id\.ToString\(\)\)/)
  })
})
