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

function spec(overrides: Partial<ScaffoldScreenControllerSpec> = {}): ScaffoldScreenControllerSpec {
  return {
    section: 'opportunites',
    entity: 'Opportunity',
    module: 'pipeline',
    appCode: 'crm',
    namespace: 'Crm',
    moduleDir: '/ba/CRM/PIPELINE',
    projectPath: '/project',
    ...overrides,
  }
}

function pagespec(overrides: Partial<PageSpec> = {}): PageSpec {
  return {
    screenCode: 'SCR-CRM-PIPELINE-OPPORTUNITES-001',
    appCode: 'crm',
    module: 'pipeline',
    section: 'opportunites',
    entity: 'Opportunity',
    view: 'list',
    permission: 'pipeline.opportunites.read',
    linkedUseCases: [],
    linkedBusinessRules: [],
    columns: [],
    actions: [],
    ...overrides,
  } as PageSpec
}

function controllerOf(files: { path: string; content: string }[]): { path: string; content: string } | undefined {
  return files.find(f => f.path.endsWith('ScreenController.cs'))
}

describe('scaffold-screen-controller / generate — controller-level attributes', () => {
  it('emits a controller named {EntityPlural}ScreenController under Controllers/{Mod}/Screens/', () => {
    const { files } = generate(spec(), [pagespec()])
    const ctrl = controllerOf(files)
    expect(ctrl?.path).toBe('src/Crm.Api/Controllers/Pipeline/Screens/OpportunitiesScreenController.cs')
  })

  it('tags the class with [ApiExplorerSettings(GroupName = "screens")]', () => {
    const { files } = generate(spec(), [pagespec()])
    expect(controllerOf(files)?.content).toMatch(/\[ApiExplorerSettings\(GroupName\s*=\s*"screens"\)\]/)
  })

  it('routes the controller under api/screens/{entityPluralLower}', () => {
    const { files } = generate(spec(), [pagespec()])
    expect(controllerOf(files)?.content).toMatch(/\[Route\("api\/screens\/opportunities"\)\]/)
  })

  it('injects I{Entity}Service as the only dependency', () => {
    const { files } = generate(spec(), [pagespec()])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/private readonly IOpportunityService _service;/)
    expect(c).toMatch(/public OpportunitiesScreenController\(IOpportunityService service\)/)
  })
})

describe('scaffold-screen-controller / generate — list view', () => {
  it('emits [HttpGet("list")] with paginated dto + read permission', () => {
    const { files } = generate(spec(), [pagespec({ view: 'list' })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[HttpGet\("list"\)\]/)
    expect(c).toMatch(/ActionResult<PaginatedResult<OpportunityListScreenDto>>/)
    expect(c).toMatch(/\[RequirePermission\(PipelinePermissions\.Opportunites\.Read\)\]/)
    expect(c).toMatch(/_service\.GetForListScreenAsync\(new GetOpportunityListScreenQuery\(page, pageSize, search\), ct\)/)
  })

  it('emits a {Entity}ListScreenDto record under DTOs/Screens/', () => {
    const { files } = generate(spec(), [pagespec({
      view: 'list',
      columns: [
        { key: 'amount', formatHint: 'currency' },
        { key: 'name', formatHint: 'string' },
      ],
    })])
    const dto = files.find(f => f.path.endsWith('OpportunityListScreenDto.cs'))!
    expect(dto.path).toBe('src/Crm.Application/Pipeline/DTOs/Screens/OpportunityListScreenDto.cs')
    expect(dto.content).toMatch(/public record OpportunityListScreenDto/)
    expect(dto.content).toMatch(/public decimal Amount \{ get; init; \} = 0;/)
    expect(dto.content).toMatch(/public string Name \{ get; init; \} = "";/)
  })

  it('handles list with no columns (safe minimum dto)', () => {
    const { files } = generate(spec(), [pagespec({ view: 'list', columns: [] })])
    const dto = files.find(f => f.path.endsWith('OpportunityListScreenDto.cs'))!
    expect(dto.content).toMatch(/public Guid Id \{ get; init; \}/)
    expect(dto.content).toMatch(/No columns in pagespec/)
  })
})

describe('scaffold-screen-controller / generate — detail view', () => {
  it('emits [HttpGet("detail/{id:guid}")] with NotFound branch', () => {
    const { files } = generate(spec(), [pagespec({ view: 'detail' })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[HttpGet\("detail\/\{id:guid\}"\)\]/)
    expect(c).toMatch(/ActionResult<OpportunityDetailScreenDto>/)
    expect(c).toMatch(/result is null \? NotFound\(\) : Ok\(result\)/)
    expect(c).toMatch(/_service\.GetForDetailScreenAsync\(id, ct\)/)
  })

  it('emits a {Entity}DetailScreenDto record', () => {
    const { files } = generate(spec(), [pagespec({ view: 'detail', columns: [{ key: 'stage' }] })])
    const dto = files.find(f => f.path.endsWith('OpportunityDetailScreenDto.cs'))!
    expect(dto.content).toMatch(/public record OpportunityDetailScreenDto/)
    expect(dto.content).toMatch(/public string Stage \{ get; init; \}/)
  })
})

describe('scaffold-screen-controller / generate — form view', () => {
  it('emits POST /form and PUT /form/{id:guid}', () => {
    const { files } = generate(spec(), [pagespec({ view: 'form', permission: 'pipeline.opportunites.update' })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[HttpPost\("form"\)\]/)
    expect(c).toMatch(/\[HttpPut\("form\/\{id:guid\}"\)\]/)
    expect(c).toMatch(/CreateOpportunityDto dto/)
    expect(c).toMatch(/UpdateOpportunityDto dto/)
    expect(c).toMatch(/_service\.CreateAsync/)
    expect(c).toMatch(/_service\.UpdateAsync/)
  })

  it('emits NO DTO for form (consumes integration Create/UpdateDto)', () => {
    const { files } = generate(spec(), [pagespec({ view: 'form' })])
    const dto = files.find(f => f.path.includes('FormScreenDto'))
    expect(dto).toBeUndefined()
  })

  it('emits CreatedAtAction pointing to GetDetail', () => {
    const { files } = generate(spec(), [pagespec({ view: 'form' })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/CreatedAtAction\(nameof\(GetDetailRoute\)/)
  })
})

describe('scaffold-screen-controller / generate — custom actions on a list view', () => {
  it('emits a row-scope POST action under detail/{id:guid}/{endpoint}', () => {
    const { files } = generate(spec(), [pagespec({
      view: 'list',
      actions: [{
        code: 'archive',
        scope: 'row',
        kind: 'api',
        httpMethod: 'POST',
        permission: 'pipeline.opportunites.update',
      }],
    })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[HttpPost\("detail\/\{id:guid\}\/archive"\)\]/)
    expect(c).toMatch(/public async Task<ActionResult> Archive\(Guid id, CancellationToken/)
    expect(c).toMatch(/_service\.ArchiveAsync\(id, ct\)/)
    expect(c).toMatch(/\[RequirePermission\(PipelinePermissions\.Opportunites\.Update\)\]/)
  })

  it('emits a bulk-scope action under bulk/{endpoint}', () => {
    const { files } = generate(spec(), [pagespec({
      view: 'list',
      actions: [{
        code: 'bulk-archive',
        scope: 'bulk',
        kind: 'api',
        httpMethod: 'POST',
        payloadDto: 'BulkArchiveRequest',
        permission: 'pipeline.opportunites.update',
      }],
    })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[HttpPost\("bulk\/bulk-archive"\)\]/)
    expect(c).toMatch(/\[FromBody\] BulkArchiveRequest dto/)
  })

  it('emits a header-scope action under {endpoint} (no id)', () => {
    const { files } = generate(spec(), [pagespec({
      view: 'list',
      actions: [{
        code: 'syncFromPce',
        scope: 'header',
        kind: 'api',
        endpoint: 'sync-from-proconcept',
        httpMethod: 'POST',
        permission: 'pipeline.opportunites.execute',
      }],
    })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[HttpPost\("sync-from-proconcept"\)\]/)
    expect(c).toMatch(/_service\.SyncFromPceAsync\(ct\)/)
  })

  it('omits [FromBody] on GET actions even when payloadDto is set', () => {
    const { files } = generate(spec(), [pagespec({
      view: 'list',
      actions: [{
        code: 'export',
        scope: 'header',
        kind: 'api',
        httpMethod: 'GET',
        payloadDto: 'ExportRequest',
        responseDto: 'FileContentResult',
        permission: 'pipeline.opportunites.read',
      }],
    })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[HttpGet\("export"\)\]/)
    expect(c).not.toMatch(/Export\([^)]*FromBody/)
  })

  it('skips navigation actions (kind="navigate") — those are pure frontend', () => {
    const { files } = generate(spec(), [pagespec({
      view: 'list',
      actions: [{ code: 'open', kind: 'navigate', scope: 'row', targetScreen: 'SCR-X-001' }],
    })])
    const c = controllerOf(files)!.content
    // Backend should NOT emit a method for a navigate action.
    expect(c).not.toMatch(/public async Task<ActionResult> Open\(/)
  })
})

describe('scaffold-screen-controller / generate — unsupported views fall back gracefully', () => {
  // Wave 1 of the URL-convention alignment (2026-05-27) reshaped this:
  //   - dashboard       → fully supported (Wave F3)
  //   - kanban / card   → MVP fallback: reuse the list endpoint + DTO so the
  //                        frontend's getAll() call lands on a real route.
  //                        A TODO marker still surfaces for the dedicated
  //                        emitter that's still to be built.
  //   - app/module/section-home → hub views; frontend emits NO API call, so
  //                        backend emits NO endpoint and NO TODO. Absence is
  //                        by design — see hub-views.test.ts for the lock.
  it('reuses the list endpoint for kanban + surfaces a single TODO for the dedicated emitter', () => {
    const { todos, files } = generate(spec(), [pagespec({ view: 'kanban' })])
    expect(todos.length).toBe(1)
    expect(todos[0]).toMatch(/view "kanban" reuses GET \/list as MVP fallback/)
    const ctrl = controllerOf(files)!
    // The kanban pagespec produces a real [HttpGet("list")] route so the
    // frontend's GET ${API_PATH}/list call never 404s.
    expect(ctrl.content).toMatch(/\[HttpGet\("list"\)\]/)
  })
})

describe('scaffold-screen-controller / generate — multiple pagespecs in one controller', () => {
  it('emits all three views (list + detail + form) in one controller class', () => {
    const { files } = generate(spec(), [
      pagespec({ view: 'list', screenCode: 'SCR-A-001' }),
      pagespec({ view: 'detail', screenCode: 'SCR-A-002' }),
      pagespec({ view: 'form', screenCode: 'SCR-A-003', permission: 'pipeline.opportunites.update' }),
    ])
    const ctrls = files.filter(f => f.path.endsWith('ScreenController.cs'))
    expect(ctrls.length).toBe(1) // ONE controller, not three
    const c = ctrls[0].content
    expect(c).toMatch(/HttpGet\("list"\)/)
    expect(c).toMatch(/HttpGet\("detail\/\{id:guid\}"\)/)
    expect(c).toMatch(/HttpPost\("form"\)/)
    expect(c).toMatch(/SCR-A-001/)
    expect(c).toMatch(/SCR-A-002/)
    expect(c).toMatch(/SCR-A-003/)
  })
})

describe('scaffold-screen-controller / generate — Wave F3 (dashboard view)', () => {
  it('emits a single /dashboard endpoint for view=dashboard (no consolidated/alerts split)', () => {
    const { files } = generate(spec(), [pagespec({ view: 'dashboard', screenCode: 'SCR-PIPE-DASH-001' })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[HttpGet\("dashboard"\)\]/)
    expect(c).not.toMatch(/dashboard\/consolidated/)
    expect(c).not.toMatch(/dashboard\/alerts/)
  })

  it('emits a self-contained OpportunityDashboardDto so the controller compiles standalone', () => {
    const { files } = generate(spec(), [pagespec({ view: 'dashboard' })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/ActionResult<OpportunityDashboardDto>/)
    const dashDto = files.find(f => /OpportunityDashboardDto\.cs$/.test(f.path))
    expect(dashDto, 'OpportunityDashboardDto.cs').toBeDefined()
    expect(dashDto!.content).toMatch(/Dictionary<string, object> Widgets/)
  })

  it('forwards startDate + endDate and returns a stub payload (no phantom service call)', () => {
    const { files } = generate(spec(), [pagespec({ view: 'dashboard' })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[FromQuery\] DateTime\? startDate = null/)
    expect(c).toMatch(/\[FromQuery\] DateTime\? endDate = null/)
    expect(c).toMatch(/return Ok\(new OpportunityDashboardDto\(\)\)/)
    expect(c).not.toMatch(/_service\.GetDashboard/)
  })

  it('applies [RequirePermission] derived from the pagespec', () => {
    const { files } = generate(spec(), [pagespec({ view: 'dashboard', permission: 'pipeline.opportunites.read' })])
    const c = controllerOf(files)!.content
    expect(c).toMatch(/\[RequirePermission\(PipelinePermissions\.Opportunites\.Read\)\]/)
  })

  it('does NOT push a TODO for view=dashboard (no longer unsupported)', () => {
    const { todos } = generate(spec(), [pagespec({ view: 'dashboard' })])
    expect(todos.find(t => /view "dashboard"/.test(t))).toBeUndefined()
  })

  it('TODO behaviour after Wave 1 URL alignment — kanban surfaces a TODO; hub views (app-home) do NOT', () => {
    // kanban falls back to [HttpGet("list")] but pushes a TODO so the
    // dedicated emitter remains on the audit radar. app-home / module-home /
    // section-home are pure Slot containers on the frontend — no API call,
    // no endpoint, no TODO (absence is by design, locked by hub-views.test.ts).
    const { todos } = generate(spec(), [
      pagespec({ view: 'kanban', screenCode: 'SCR-K-1' }),
      pagespec({ view: 'app-home', screenCode: 'SCR-H-1' }),
    ])
    expect(todos.some(t => /view "kanban"/.test(t))).toBe(true)
    expect(todos.some(t => /view "app-home"/.test(t))).toBe(false)
  })

  it('dashboard endpoints sit alongside list/detail/form in the same controller', () => {
    const { files } = generate(spec(), [
      pagespec({ view: 'list', screenCode: 'SCR-L' }),
      pagespec({ view: 'detail', screenCode: 'SCR-D' }),
      pagespec({ view: 'form', screenCode: 'SCR-F', permission: 'pipeline.opportunites.update' }),
      pagespec({ view: 'dashboard', screenCode: 'SCR-DASH' }),
    ])
    const ctrls = files.filter(f => f.path.endsWith('ScreenController.cs'))
    expect(ctrls.length).toBe(1)
    const c = ctrls[0].content
    expect(c).toMatch(/HttpGet\("list"\)/)
    expect(c).toMatch(/HttpGet\("detail\/\{id:guid\}"\)/)
    expect(c).toMatch(/HttpPost\("form"\)/)
    expect(c).toMatch(/HttpGet\("dashboard"\)/)
  })
})
