/**
 * cli:scaffold-tests — generate.ts
 * Generates test files per layer following SmartStack.app test conventions.
 * Tests consume NuGet packages (SmartStack.Core, SmartStack.Application) and standard
 * .NET test libraries (xUnit, FluentAssertions, Moq, FluentValidation.TestHelper,
 * Microsoft.EntityFrameworkCore.InMemory). Frontend tests consume npm @atlashub/smartstack.
 */
import type { ScaffoldTestsInput, GeneratedFile, EntityTestSpec, BusinessRuleTest } from './types.js'

export function generate(spec: ScaffoldTestsInput): GeneratedFile[] {
  switch (spec.layer) {
    case 'seed': return generateSeedTests(spec)
    case 'domain': return generateDomainTests(spec)
    case 'business': return generateBusinessTests(spec)
    case 'api': return generateApiTests(spec)
    case 'frontend': return generateFrontendTests(spec)
  }
}

function generateSeedTests(spec: ScaffoldTestsInput): GeneratedFile[] {
  const ns = spec.namespace ?? `${spec.appCode}.Tests.${capitalize(spec.module)}`
  const className = `${capitalize(spec.module)}SeedDataProviderTests`
  const providerClass = `${capitalize(spec.module)}SeedDataProvider`

  const content = `using Xunit;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Interfaces;
using SmartStack.Infrastructure.Persistence;
using ${spec.appCode}.Infrastructure.Seeding;

namespace ${ns}.Seed;

[Trait("Category", "Seed")]
[Trait("Type", "Unit")]
public class ${className}
{
    private static CoreDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<CoreDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        return new CoreDbContext(options);
    }

    [Fact]
    public async Task SeedNavigationAsync_ShouldBeIdempotent()
    {
        await using var context = CreateContext();
        var provider = new ${providerClass}();

        await provider.SeedNavigationAsync(context, TestContext.Current.CancellationToken);
        var firstCount = await context.NavigationApplications.CountAsync();

        await provider.SeedNavigationAsync(context, TestContext.Current.CancellationToken);
        var secondCount = await context.NavigationApplications.CountAsync();

        secondCount.Should().Be(firstCount, "seed must be idempotent across multiple invocations");
    }

    [Fact]
    public async Task SeedNavigationAsync_ShouldCreateFullHierarchy()
    {
        await using var context = CreateContext();
        var provider = new ${providerClass}();

        await provider.SeedNavigationAsync(context, TestContext.Current.CancellationToken);

        var apps = await context.NavigationApplications.ToListAsync();
        var modules = await context.NavigationModules.ToListAsync();

        apps.Should().NotBeEmpty("the provider must seed at least one application");
        modules.Should().NotBeEmpty("the provider must seed at least one module");
        modules.Should().OnlyContain(m => apps.Any(a => a.Id == m.ApplicationId),
            "every module must reference a seeded application");
    }

    [Fact]
    public async Task SeedPermissionsAsync_ShouldCoverAllSections()
    {
        await using var context = CreateContext();
        var provider = new ${providerClass}();

        await provider.SeedNavigationAsync(context, TestContext.Current.CancellationToken);
        await provider.SeedPermissionsAsync(context, TestContext.Current.CancellationToken);

        var sections = await context.NavigationSections.Select(s => s.Code).ToListAsync();
        var permissionPaths = await context.NavigationPermissions.Select(p => p.Path).ToListAsync();

        foreach (var section in sections)
        {
            permissionPaths.Should().Contain(p => p.Contains(section),
                $"section '{section}' must have at least one permission");
        }
    }

    [Fact]
    public async Task SeedRolePermissionsAsync_ShouldMapAllRoles()
    {
        await using var context = CreateContext();
        var provider = new ${providerClass}();

        await provider.SeedNavigationAsync(context, TestContext.Current.CancellationToken);
        await provider.SeedPermissionsAsync(context, TestContext.Current.CancellationToken);
        await provider.SeedRolesAsync(context, TestContext.Current.CancellationToken);
        await provider.SeedRolePermissionsAsync(context, TestContext.Current.CancellationToken);

        var roles = await context.Roles.ToListAsync();
        var rolePermissions = await context.RolePermissions.ToListAsync();

        roles.Should().NotBeEmpty();
        foreach (var role in roles)
        {
            rolePermissions.Should().Contain(rp => rp.RoleId == role.Id,
                $"role '{role.Code}' must have at least one mapped permission");
        }
    }
}
`
  return [{ path: `Tests/${capitalize(spec.module)}/Seed/${className}.cs`, content }]
}

function generateDomainTests(spec: ScaffoldTestsInput): GeneratedFile[] {
  return spec.entities.map(entity => {
    const ns = spec.namespace ?? `${spec.appCode}.Tests.${capitalize(spec.module)}`
    const className = `${entity.name}Tests`
    const requiredFields = entity.fields.filter(f => f.required)

    const content = `using Xunit;
using FluentAssertions;
using ${spec.appCode}.Domain.Entities;

namespace ${ns}.Domain;

[Trait("Category", "Domain")]
[Trait("Type", "Unit")]
public class ${className}
{
    #region Create

    [Fact]
    public void Create_WithValidParams_ShouldSucceed()
    {
        var before = DateTime.UtcNow;

        var entity = ${entity.name}.Create(${requiredFields.map(f => getDefaultValue(f.type)).join(', ')});

        entity.Should().NotBeNull();
        entity.Id.Should().NotBeEmpty();
        entity.CreatedAt.Should().BeOnOrAfter(before);
${requiredFields.map(f => `        entity.${capitalize(f.name)}.Should().${getAssertionForType(f.type, f.name)};`).join('\n')}
    }

    [Fact]
    public void Create_WithInvalidParams_ShouldThrow()
    {
        var act = () => ${entity.name}.Create(${requiredFields.map(f => getNullValue(f.type)).join(', ')});

        act.Should().Throw<ArgumentException>();
    }

    [Fact]
    public void Create_ShouldGenerateUniqueIds()
    {
        var a = ${entity.name}.Create(${requiredFields.map(f => getDefaultValue(f.type)).join(', ')});
        var b = ${entity.name}.Create(${requiredFields.map(f => getDefaultValue(f.type)).join(', ')});

        a.Id.Should().NotBe(b.Id);
    }

    #endregion
}
`
    return { path: `Tests/${capitalize(spec.module)}/Domain/${className}.cs`, content }
  })
}

function generateBusinessTests(spec: ScaffoldTestsInput): GeneratedFile[] {
  const files: GeneratedFile[] = []
  const app = spec.appCode
  const mod = capitalize(spec.module)
  const ns = spec.namespace ?? `${app}.Tests.${mod}`     // test namespace
  const appNs = `${app}.Application.${mod}`              // production application namespace
  const svcNs = `${app}.Infrastructure.Services.${mod}`  // production service namespace

  // Shared tenant test-double — emitted ONCE. Pins a fixed tenant id so the
  // ExtensionsDbContext row-level tenant filter is active in the in-memory tests
  // (EF Core InMemory HONOURS query filters).
  files.push({
    path: `Tests/Common/FakeCurrentTenantService.cs`,
    content: `using SmartStack.Application.Common.Interfaces.Tenants;
using SmartStack.Domain.Platform.Administration.Tenants;

namespace ${app}.Tests.Common;

/// <summary>
/// Test double for <see cref="ICurrentTenantService"/>. Pins a fixed tenant id so
/// the ExtensionsDbContext tenant query filter is exercised in unit tests.
/// </summary>
public sealed class FakeCurrentTenantService : ICurrentTenantService
{
    public FakeCurrentTenantService(Guid? tenantId = null) => TenantId = tenantId ?? Guid.NewGuid();

    public Guid? TenantId { get; private set; }
    public string? TenantSlug => null;
    public Tenant? Tenant => null;
    public bool HasTenant => TenantId.HasValue;
    public bool HasGlobalAccess => false;
    public bool IsGlobalScope => !TenantId.HasValue;

    public Task<bool> SetBySlugAsync(string slug, CancellationToken cancellationToken = default) => Task.FromResult(false);
    public Task<bool> SetByIdAsync(Guid tenantId, CancellationToken cancellationToken = default)
    {
        TenantId = tenantId;
        return Task.FromResult(true);
    }
    public void SetTenant(Tenant? tenant) { }
    public void SetGlobalAccess(bool hasGlobalAccess) { }
    public void Clear() => TenantId = null;
}
`,
  })

  for (const entity of spec.entities) {
    const e = entity.name
    const entityRules = spec.businessRules.filter(r => r.entityName === e)
    const requiredFields = entity.fields.filter(f => f.required)
    const defaultArgs = requiredFields.map(f => getDefaultValue(f.type)).join(', ')
    const hasRequiredString = requiredFields.some(f => f.type.toLowerCase() === 'string')

    // ── Handler tests — the handler is a thin delegator to I{E}Service, so we
    //    mock the service and Verify the delegation (canonical "no-query handler"). ──
    files.push({
      path: `Tests/${mod}/Application/${e}HandlersTests.cs`,
      content: `using Xunit;
using FluentAssertions;
using Moq;
using ${appNs}.Commands;
using ${appNs}.Handlers;
using ${appNs}.Interfaces;

namespace ${ns}.Application;

[Trait("Category", "Business")]
[Trait("Type", "Unit")]
public class ${e}HandlersTests
{
    private readonly Mock<I${e}Service> _serviceMock = new();

    #region Create${e}CommandHandler

    [Fact]
    public async Task Handle_ValidCommand_DelegatesToServiceCreate()
    {
        // Arrange
        var command = new Create${e}Command(${defaultArgs});
        var expectedId = Guid.NewGuid();
        _serviceMock
            .Setup(s => s.CreateAsync(It.IsAny<Create${e}Command>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(expectedId);
        var handler = new Create${e}CommandHandler(_serviceMock.Object);

        // Act
        var result = await handler.Handle(command, TestContext.Current.CancellationToken);

        // Assert
        result.Should().Be(expectedId);
        _serviceMock.Verify(s => s.CreateAsync(command, It.IsAny<CancellationToken>()), Times.Once);
    }

    #endregion

    #region Delete${e}CommandHandler

    [Fact]
    public async Task Handle_DeleteCommand_DelegatesToServiceDelete()
    {
        // Arrange
        var id = Guid.NewGuid();
        var handler = new Delete${e}CommandHandler(_serviceMock.Object);

        // Act
        await handler.Handle(new Delete${e}Command(id), TestContext.Current.CancellationToken);

        // Assert
        _serviceMock.Verify(s => s.DeleteAsync(id, It.IsAny<CancellationToken>()), Times.Once);
    }

    #endregion
}
`,
    })

    // ── Service tests — real in-memory ExtensionsDbContext + fixed tenant. ──
    files.push({
      path: `Tests/${mod}/Application/${e}ServiceTests.cs`,
      content: `using Xunit;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using ${app}.Infrastructure.Persistence;
using ${svcNs};
using ${appNs}.Commands;
using ${app}.Domain.Entities;
using ${app}.Tests.Common;

namespace ${ns}.Application;

[Trait("Category", "Business")]
[Trait("Type", "Unit")]
public class ${e}ServiceTests
{
    // In-memory ExtensionsDbContext, wired with a fixed tenant so the tenant query
    // filter is exercised. EF Core InMemory HONOURS query filters; it does NOT enforce
    // relational constraints (FK/unique), SQL translation, or SqlObjects/TVF — those are
    // covered by the integration tests on real SQL Server LocalDB.
    private static ${e}Service CreateService(out ExtensionsDbContext context)
    {
        var options = new DbContextOptionsBuilder<ExtensionsDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        context = new ExtensionsDbContext(options, new FakeCurrentTenantService(Guid.NewGuid()));
        return new ${e}Service(context);
    }

    #region CreateAsync

    [Fact]
    public async Task CreateAsync_ValidCommand_PersistsEntity()
    {
        // Arrange
        var service = CreateService(out var context);
        var command = new Create${e}Command(${defaultArgs});

        // Act
        var id = await service.CreateAsync(command, TestContext.Current.CancellationToken);

        // Assert
        id.Should().NotBeEmpty();
        var persisted = await context.Set<${e}>().FindAsync(new object[] { id }, TestContext.Current.CancellationToken);
        persisted.Should().NotBeNull();
    }

    #endregion

    #region GetByIdAsync

    [Fact]
    public async Task GetByIdAsync_AfterCreate_ReturnsTheEntity()
    {
        // Arrange
        var service = CreateService(out _);
        var id = await service.CreateAsync(new Create${e}Command(${defaultArgs}), TestContext.Current.CancellationToken);

        // Act
        var dto = await service.GetByIdAsync(id, TestContext.Current.CancellationToken);

        // Assert
        dto.Should().NotBeNull();
        dto!.Id.Should().Be(id);
    }

    #endregion

    #region DeleteAsync

    [Fact]
    public async Task DeleteAsync_AfterCreate_RemovesTheEntity()
    {
        // Arrange
        var service = CreateService(out var context);
        var id = await service.CreateAsync(new Create${e}Command(${defaultArgs}), TestContext.Current.CancellationToken);

        // Act
        await service.DeleteAsync(id, TestContext.Current.CancellationToken);

        // Assert
        var afterDelete = await context.Set<${e}>().FindAsync(new object[] { id }, TestContext.Current.CancellationToken);
        afterDelete.Should().BeNull();
    }

    #endregion
}
`,
    })

    // ── Validator tests — required fields + one [Fact] per business rule. ──
    const requiredFieldTests = requiredFields.map(f => {
      const invalidArgs = requiredFields.map(rf => rf.name === f.name ? getNullValue(rf.type) : getDefaultValue(rf.type))
      return `    [Fact]
    public void Validate_Empty${capitalize(f.name)}_ShouldFail()
    {
        var command = new Create${e}Command(${invalidArgs.join(', ')});

        var result = _validator.TestValidate(command);

        result.ShouldHaveValidationErrorFor(x => x.${capitalize(f.name)});
    }`
    }).join('\n\n')

    // One [Fact] per business rule. Gated on having ≥1 required string field so the
    // all-empty command is genuinely rejected by a NotEmpty rule — a real assertion,
    // never a stub. Refine each to assert the SPECIFIC field/error once modelled.
    const ruleTests = hasRequiredString
      ? entityRules.map(rule => `    /// <summary>${rule.description}</summary>
    [Fact]
    public void Validate_Enforces_${sanitizeMethodName(rule.id)}()
    {
        // Business rule ${rule.id}: ${rule.description}
${(rule.invalidExamples.length > 0 ? rule.invalidExamples.map(ex => `        // Invalid: ${ex}`).join('\n') : '        // Invalid: an incomplete command must be rejected.')}
        var invalid = new Create${e}Command(${requiredFields.map(f => getNullValue(f.type)).join(', ')});

        _validator.TestValidate(invalid).IsValid.Should().BeFalse("rule '${rule.id}' must reject an invalid command");
    }`).join('\n\n')
      : ''

    files.push({
      path: `Tests/${mod}/Application/Create${e}CommandValidatorTests.cs`,
      content: `using Xunit;
using FluentAssertions;
using FluentValidation.TestHelper;
using ${appNs}.Commands;
using ${appNs}.Validators;

namespace ${ns}.Application;

[Trait("Category", "Business")]
[Trait("Type", "Unit")]
public class Create${e}CommandValidatorTests
{
    private readonly Create${e}CommandValidator _validator = new();

    [Fact]
    public void Validate_ValidCommand_ShouldPass()
    {
        var command = new Create${e}Command(${defaultArgs});

        var result = _validator.TestValidate(command);

        result.ShouldNotHaveAnyValidationErrors();
    }

${requiredFieldTests}${ruleTests ? '\n\n' + ruleTests : ''}
}
`,
    })
  }

  return files
}

function generateApiTests(spec: ScaffoldTestsInput): GeneratedFile[] {
  const app = spec.appCode
  const mod = capitalize(spec.module)
  const ns = spec.namespace ?? `${app}.Tests.${mod}`
  const files: GeneratedFile[] = []

  // Integration harness — emitted ONCE (idempotent), shared by every module.
  // REAL SQL Server LocalDB + Respawn + a WebApplicationFactory whose JWT is
  // overridden via PostConfigure. This layer catches what EF InMemory cannot
  // (relational constraints, SQL translation, SqlObjects/TVF).
  files.push(...integrationHarnessFiles(app))

  for (const entity of spec.entities) {
    const className = `${entity.pluralName}ControllerTests`
    const routeBase = `/api/${spec.module.toLowerCase()}/${entity.pluralName.toLowerCase()}`
    files.push({
      path: `Tests/${mod}/Api/${className}.cs`,
      content: `using System.Net;
using System.Net.Http.Headers;
using FluentAssertions;
using Xunit;
using ${app}.Tests.Common;

namespace ${ns}.Api;

[Trait("Category", "Integration")]
[Trait("Type", "Integration")]
[Collection("Integration")]
public class ${className} : IAsyncDisposable
{
    private readonly DatabaseFixture _db;
    private readonly ${app}WebAppFactory _factory;
    private readonly HttpClient _client;

    public ${className}(DatabaseFixture db)
    {
        _db = db;
        _factory = new ${app}WebAppFactory(db.ConnectionString);
        _client = _factory.CreateClient();
    }

    private HttpClient Authenticated(params string[] permissions)
    {
        var client = _factory.CreateClient();
        var token = JwtTokenHelper.GenerateToken(permissions: permissions);
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return client;
    }

    [Fact]
    public async Task GetAll_Unauthenticated_Returns401()
    {
        // No token → the API auth pipeline rejects before reaching the route.
        var response = await _client.GetAsync("${routeBase}");

        response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
    }

    [Fact]
    public async Task GetAll_Authenticated_HitsTheRealRoute()
    {
        await _db.ResetAsync();
        var client = Authenticated("${spec.module}.view");

        var response = await client.GetAsync("${routeBase}");

        // 200 when the token carries the required permission and the API does not
        // enforce a server-side session; 401/403 otherwise. Seed a real session /
        // permission row (via _db.CreateContext()) if your API enforces those.
        response.StatusCode.Should().BeOneOf(
            HttpStatusCode.OK, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden);
    }

    [Fact]
    public async Task GetById_UnknownId_DoesNotServerError()
    {
        await _db.ResetAsync();
        var client = Authenticated("${spec.module}.view");

        var response = await client.GetAsync($"${routeBase}/{Guid.NewGuid()}");

        // The route exists and handles an unknown id without crashing (no 5xx).
        ((int)response.StatusCode).Should().BeLessThan(500);
    }

    public async ValueTask DisposeAsync()
    {
        _client.Dispose();
        await _factory.DisposeAsync();
    }
}
`,
    })
  }

  return files
}

/**
 * The shared integration harness (emitted once into Tests/Common/). A real
 * SQL Server LocalDB + Respawn + a WebApplicationFactory whose JWT validation is
 * overridden via PostConfigure (NOT config — read too early by AddInfrastructure).
 *
 * Requires these NuGet packages in the client test project: Respawn,
 * Microsoft.Data.SqlClient, Microsoft.AspNetCore.Mvc.Testing,
 * System.IdentityModel.Tokens.Jwt.
 *
 * Execution pitfalls (documented for the client):
 *  - A running dev API locks the build output → MSB3021. Stop it, or
 *    `dotnet test -p:BuildProjectReferences=false`.
 *  - Use `npm install` (not `npm ci`) while a Vite dev server runs
 *    (@tailwindcss/oxide lock).
 */
function integrationHarnessFiles(app: string): GeneratedFile[] {
  const databaseFixture = `using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Respawn;
using Xunit;
using ${app}.Infrastructure.Persistence;

namespace ${app}.Tests.Common;

/// <summary>
/// Per-collection SQL Server LocalDB fixture. Creates a throwaway database,
/// applies migrations, and resets the 'extensions' schema between tests with
/// Respawn. A real relational engine — catches FK/unique constraints, SQL
/// translation and SqlObjects/TVF that EF InMemory cannot.
/// ADJUST: the LocalDB instance / connection string for your CI.
/// </summary>
public sealed class DatabaseFixture : IAsyncLifetime
{
    public string ConnectionString { get; } =
        $"Server=(localdb)\\\\MSSQLLocalDB;Database=Ext_IntTests_{Guid.NewGuid():N};Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=true";

    private Respawner? _respawner;

    public async ValueTask InitializeAsync()
    {
        await using var ctx = CreateContext();
        await ctx.Database.MigrateAsync();

        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();
        _respawner = await Respawner.CreateAsync(conn, new RespawnerOptions
        {
            DbAdapter = DbAdapter.SqlServer,
            SchemasToInclude = new[] { "extensions" },
            WithReseed = true,
        });
    }

    /// <summary>Reset the extension tables to empty between tests.</summary>
    public async Task ResetAsync()
    {
        if (_respawner is null) return;
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();
        await _respawner.ResetAsync(conn);
    }

    /// <summary>A context bound to the test database, in GLOBAL scope (no tenant filter) for seeding.</summary>
    public ExtensionsDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ExtensionsDbContext>()
            .UseSqlServer(ConnectionString)
            .Options;
        return new ExtensionsDbContext(options);
    }

    public async ValueTask DisposeAsync()
    {
        await using var ctx = CreateContext();
        await ctx.Database.EnsureDeletedAsync();
    }
}

/// <summary>Bind every integration test class to the single LocalDB fixture.</summary>
[CollectionDefinition("Integration")]
public sealed class IntegrationCollection : ICollectionFixture<DatabaseFixture> { }
`

  const webAppFactory = `using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using ${app}.Infrastructure.Persistence;

namespace ${app}.Tests.Common;

/// <summary>
/// In-process API host bound to the integration test database. JWT validation is
/// overridden via PostConfigure — the ONLY reliable hook, because AddInfrastructure
/// reads the JWT config before ConfigureAppConfiguration runs. Tests mint tokens
/// with <see cref="JwtTokenHelper"/> using the same key.
///
/// ADJUST: the config keys (Jwt:*, ConnectionStrings:*) and the DbContextOptions
/// service type to match your API's registration. Requires the API project to
/// expose 'public partial class Program { }' and be referenced by this test project.
/// </summary>
public sealed class ${app}WebAppFactory : WebApplicationFactory<Program>
{
    private readonly string _connectionString;

    public ${app}WebAppFactory(string connectionString) => _connectionString = connectionString;

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Development");

        builder.ConfigureAppConfiguration((_, config) =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ConnectionStrings:DefaultConnection"] = _connectionString,
                ["Jwt:Secret"] = JwtTokenHelper.Secret,
                ["Jwt:Issuer"] = JwtTokenHelper.Issuer,
                ["Jwt:Audience"] = JwtTokenHelper.Audience,
            });
        });

        builder.ConfigureTestServices(services =>
        {
            // Point ExtensionsDbContext at the integration test database.
            var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<ExtensionsDbContext>));
            if (descriptor is not null) services.Remove(descriptor);
            services.AddDbContext<ExtensionsDbContext>(o => o.UseSqlServer(_connectionString));

            // Override JWT AFTER AddJwtBearer — config-based override is read too early.
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtTokenHelper.Secret));
            services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, o =>
            {
                o.TokenValidationParameters.IssuerSigningKey = key;
                o.TokenValidationParameters.ValidIssuer = JwtTokenHelper.Issuer;
                o.TokenValidationParameters.ValidAudience = JwtTokenHelper.Audience;
                o.TokenValidationParameters.ValidateIssuerSigningKey = true;
            });
        });
    }
}
`

  const jwtHelper = `using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

namespace ${app}.Tests.Common;

/// <summary>
/// HMAC-SHA256 token minting for integration tests — the same key the WebAppFactory
/// installs via PostConfigure, so the tokens validate against the in-process host.
/// </summary>
public static class JwtTokenHelper
{
    public const string Secret = "IntegrationTestSecretKeyThatIsAtLeast256BitsLongForHmacSha256Validation!";
    public const string Issuer = "SmartStack";
    public const string Audience = "SmartStack";

    public static string GenerateToken(Guid? userId = null, string? email = null, IEnumerable<string>? permissions = null)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Secret));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, (userId ?? Guid.NewGuid()).ToString()),
            new(JwtRegisteredClaimNames.Email, email ?? "test@smartstack.local"),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };
        foreach (var permission in permissions ?? Array.Empty<string>())
            claims.Add(new Claim("permission", permission));

        var token = new JwtSecurityToken(
            issuer: Issuer,
            audience: Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddHours(1),
            signingCredentials: creds);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}
`

  return [
    { path: `Tests/Common/DatabaseFixture.cs`, content: databaseFixture },
    { path: `Tests/Common/${app}WebAppFactory.cs`, content: webAppFactory },
    { path: `Tests/Common/JwtTokenHelper.cs`, content: jwtHelper },
  ]
}

function generateFrontendTests(spec: ScaffoldTestsInput): GeneratedFile[] {
  return spec.entities.map(entity => {
    const entityLower = entity.name.charAt(0).toLowerCase() + entity.name.slice(1)

    const content = `import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { SmartStackProvider } from '@atlashub/smartstack';

// vi.mock is hoisted — declare it ABOVE the page import so the page picks the mock
// up. react-i18next is ALWAYS mocked (no real i18n instance → no NO_I18NEXT_INSTANCE
// warning); t(key) returns the key, so assertions match on the i18n key.
vi.mock('react-i18next', () => ({
  useTranslation: () => ({ t: (key: string) => key, i18n: { language: 'en', changeLanguage: vi.fn() } }),
}));
vi.mock('@/api/${spec.module}/use${entity.pluralName}', () => ({
  use${entity.pluralName}: () => ({ data: { items: [], total: 0 }, isLoading: false, error: null }),
}));

import { ${entity.name}ListPage } from '@/pages/${spec.module}/${entityLower}/${entity.name}ListPage';

function renderWithProviders(ui: React.ReactElement) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false, staleTime: Infinity } },
  });
  return render(
    <MemoryRouter>
      <QueryClientProvider client={queryClient}>
        <SmartStackProvider>{ui}</SmartStackProvider>
      </QueryClientProvider>
    </MemoryRouter>
  );
}

describe('${entity.name}ListPage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('renders the page heading', async () => {
    renderWithProviders(<${entity.name}ListPage />);

    expect(await screen.findByRole('heading')).toBeInTheDocument();
  });

  it('renders an empty list when the data source returns no items', async () => {
    renderWithProviders(<${entity.name}ListPage />);

    // The mocked hook returns zero items → no data rows should be rendered.
    await screen.findByRole('heading');
    expect(screen.queryByTestId('${entityLower}-row')).not.toBeInTheDocument();
  });
});
`
    return { path: `tests/${spec.module}/${entityLower}/${entity.name}.test.tsx`, content }
  })
}

// ─── Helpers ───

function capitalize(s: string): string {
  return s.charAt(0).toUpperCase() + s.slice(1)
}

function sanitizeMethodName(s: string): string {
  return s.replace(/[^a-zA-Z0-9]/g, '_')
}

function getDefaultValue(type: string): string {
  switch (type.toLowerCase()) {
    case 'string': return '"Test"'
    case 'int': case 'integer': case 'number': return '1'
    case 'decimal': return '100m'
    case 'bool': case 'boolean': return 'true'
    case 'datetime': return 'DateTime.UtcNow'
    case 'guid': return 'Guid.NewGuid()'
    default: return '"test"'
  }
}

function getNullValue(type: string): string {
  switch (type.toLowerCase()) {
    case 'string': return 'null!'
    case 'int': case 'integer': case 'number': return '0'
    case 'decimal': return '0m'
    case 'bool': case 'boolean': return 'false'
    case 'datetime': return 'default'
    case 'guid': return 'Guid.Empty'
    default: return 'null!'
  }
}

function getAssertionForType(type: string, fieldName: string): string {
  switch (type.toLowerCase()) {
    case 'string': return `Be("Test")`
    case 'int': case 'integer': case 'number': return `Be(1)`
    case 'decimal': return `Be(100m)`
    case 'bool': case 'boolean': return `BeTrue()`
    case 'datetime': return `BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5))`
    case 'guid': return `NotBeEmpty()`
    default: return `NotBeNull()`
  }
}
