/**
 * 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, CancellationToken.None);
        var firstCount = await context.NavigationApplications.CountAsync();

        await provider.SeedNavigationAsync(context, CancellationToken.None);
        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, CancellationToken.None);

        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, CancellationToken.None);
        await provider.SeedPermissionsAsync(context, CancellationToken.None);

        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, CancellationToken.None);
        await provider.SeedPermissionsAsync(context, CancellationToken.None);
        await provider.SeedRolesAsync(context, CancellationToken.None);
        await provider.SeedRolePermissionsAsync(context, CancellationToken.None);

        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}
{
    [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);
    }
}
`
    return { path: `Tests/${capitalize(spec.module)}/Domain/${className}.cs`, content }
  })
}

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

  for (const entity of spec.entities) {
    const entityRules = spec.businessRules.filter(r => r.entityName === entity.name)
    const serviceTestClass = `${entity.name}ServiceTests`
    const requiredFields = entity.fields.filter(f => f.required)

    let serviceContent = `using Xunit;
using FluentAssertions;
using Moq;
using MediatR;
using SmartStack.Application.Common.Interfaces;
using ${spec.appCode}.Application.${capitalize(spec.module)}.Commands;
using ${spec.appCode}.Domain.Entities;

namespace ${ns}.Application;

[Trait("Category", "Business")]
[Trait("Type", "Unit")]
public class ${serviceTestClass}
{
    private readonly Mock<ICoreDbContext> _contextMock = new();

    [Fact]
    public async Task Create_WithValidCommand_ShouldPersistEntity()
    {
        var command = new Create${entity.name}Command(${requiredFields.map(f => getDefaultValue(f.type)).join(', ')});
        var handler = new Create${entity.name}CommandHandler(_contextMock.Object);

        var result = await handler.Handle(command, CancellationToken.None);

        result.Should().NotBeEmpty();
    }
`
    for (const rule of entityRules) {
      serviceContent += `
    /// <summary>${rule.description}</summary>
    [Fact]
    public async Task Create_ShouldEnforce_${sanitizeMethodName(rule.id)}()
    {
        // Business Rule: ${rule.description}
${rule.validExamples.map(e => `        // Valid: ${e}`).join('\n')}
${rule.invalidExamples.map(e => `        // Invalid: ${e}`).join('\n')}
        var handler = new Create${entity.name}CommandHandler(_contextMock.Object);
${rule.invalidExamples.length > 0 ? `
        var invalidCommand = new Create${entity.name}Command(${requiredFields.map(f => getNullValue(f.type)).join(', ')});
        var act = async () => await handler.Handle(invalidCommand, CancellationToken.None);

        await act.Should().ThrowAsync<Exception>("business rule '${rule.id}' must be enforced");` : `
        var validCommand = new Create${entity.name}Command(${requiredFields.map(f => getDefaultValue(f.type)).join(', ')});
        var result = await handler.Handle(validCommand, CancellationToken.None);

        result.Should().NotBeEmpty("valid example must succeed");`}
    }
`
    }

    serviceContent += `}
`
    files.push({ path: `Tests/${capitalize(spec.module)}/Application/${serviceTestClass}.cs`, content: serviceContent })

    const validatorTestClass = `Create${entity.name}CommandValidatorTests`
    const validatorContent = `using Xunit;
using FluentAssertions;
using FluentValidation.TestHelper;
using ${spec.appCode}.Application.${capitalize(spec.module)}.Commands;
using ${spec.appCode}.Application.${capitalize(spec.module)}.Validators;

namespace ${ns}.Application;

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

    [Fact]
    public void Validate_ValidCommand_ShouldPass()
    {
        var command = new Create${entity.name}Command(${requiredFields.map(f => getDefaultValue(f.type)).join(', ')});

        var result = _validator.TestValidate(command);

        result.ShouldNotHaveAnyValidationErrors();
    }

${entity.fields.filter(f => f.required).map(f => {
      const validArgs = 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${entity.name}Command(${validArgs.join(', ')});

        var result = _validator.TestValidate(command);

        result.ShouldHaveValidationErrorFor(x => x.${capitalize(f.name)});
    }`
    }).join('\n\n')}
}
`
    files.push({ path: `Tests/${capitalize(spec.module)}/Application/${validatorTestClass}.cs`, content: validatorContent })
  }

  return files
}

function generateApiTests(spec: ScaffoldTestsInput): GeneratedFile[] {
  return spec.entities.map(entity => {
    const ns = spec.namespace ?? `${spec.appCode}.Tests.${capitalize(spec.module)}`
    const className = `${entity.pluralName}ControllerTests`
    const routeBase = `/api/${spec.module.toLowerCase()}/${entity.pluralName.toLowerCase()}`
    const requiredFields = entity.fields.filter(f => f.required)
    const sampleDto = requiredFields.map(f => `${capitalize(f.name)} = ${getDefaultValue(f.type)}`).join(', ')

    const content = `using Xunit;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using ${spec.appCode}.Application.${capitalize(spec.module)}.Commands;

namespace ${ns}.Api;

[Trait("Category", "Integration")]
[Trait("Type", "Integration")]
public class ${className} : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    // The client project should provide an AuthTestHelper that returns an HttpClient
    // with a valid JWT bearer token. See SmartStack.Tests.Integration/Helpers/JwtTokenHelper
    // in the SDK repository for a reference implementation.
    private HttpClient CreateAuthenticatedClient()
    {
        var client = _factory.CreateClient();
        // client.DefaultRequestHeaders.Authorization = AuthTestHelper.BuildBearer(roles: ["${spec.module}.admin"]);
        return client;
    }

    public ${className}(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetAll_Authenticated_ShouldReturn200()
    {
        var authed = CreateAuthenticatedClient();

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

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

    [Fact]
    public async Task GetAll_Unauthenticated_ShouldReturn401()
    {
        var response = await _client.GetAsync("${routeBase}");

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

    [Fact]
    public async Task GetById_NotFound_ShouldReturn404()
    {
        var authed = CreateAuthenticatedClient();

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

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

    [Fact]
    public async Task Create_ValidDto_ShouldReturn201()
    {
        var authed = CreateAuthenticatedClient();
        var dto = new Create${entity.name}Dto { ${sampleDto} };

        var response = await authed.PostAsJsonAsync("${routeBase}", dto);

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

    [Fact]
    public async Task Create_InvalidDto_ShouldReturn400()
    {
        var authed = CreateAuthenticatedClient();
        var dto = new Create${entity.name}Dto();

        var response = await authed.PostAsJsonAsync("${routeBase}", dto);

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

    [Fact]
    public async Task Create_WithoutPermission_ShouldReturn403()
    {
        // This test requires a client with an authenticated token lacking the '${spec.module}.create' permission.
        // The client project must provide AuthTestHelper.BuildBearer(roles: []) or similar.
        var limitedClient = _factory.CreateClient();
        var dto = new Create${entity.name}Dto { ${sampleDto} };

        var response = await limitedClient.PostAsJsonAsync("${routeBase}", dto);

        response.StatusCode.Should().BeOneOf(HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden);
    }

    [Fact]
    public async Task Update_NotFound_ShouldReturn404()
    {
        var authed = CreateAuthenticatedClient();
        var dto = new Update${entity.name}Dto { ${sampleDto} };

        var response = await authed.PutAsJsonAsync($"${routeBase}/{Guid.NewGuid()}", dto);

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

    [Fact]
    public async Task Delete_NotFound_ShouldReturn404()
    {
        var authed = CreateAuthenticatedClient();

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

        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }
}
`
    return { path: `Tests/${capitalize(spec.module)}/Api/${className}.cs`, content }
  })
}

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

    const content = `import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { SmartStackProvider } from '@atlashub/smartstack';
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>
  );
}

vi.mock('@/api/${spec.module}/use${entity.pluralName}', () => ({
  use${entity.pluralName}: () => ({
    data: { items: [], total: 0 },
    isLoading: false,
    error: null,
  }),
}));

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

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

    await waitFor(() => {
      expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
    });
  });

  it('should render the data table container', async () => {
    renderWithProviders(<${entity.name}ListPage />);

    await waitFor(() => {
      expect(screen.getByRole('table') || screen.getByTestId('${entityLower}-list')).toBeTruthy();
    });
  });

  it('should display an empty state when no data', async () => {
    renderWithProviders(<${entity.name}ListPage />);

    await waitFor(() => {
      const empty = screen.queryByText(/no data|aucune donn/i);
      expect(empty).toBeTruthy();
    });
  });
});

describe('use${entity.pluralName}', () => {
  it('should be mockable via vi.mock', () => {
    // The hook is mocked at module level above — this test documents the contract.
    expect(true).toBe(true);
  });
});
`
    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()`
  }
}
