import { describe, it, expect } from 'vitest';
import { generate, patchDependencyInjection } from '../generate.js';
import {
  CORE_PROVIDER_SUFFIXES,
  coreProviderClassName,
  diBeginMarker,
  diEndMarker,
  type CoreSeedSpec,
} from '../types.js';

function fixtureSingleApp(): CoreSeedSpec {
  return {
    appCode: 'TestV2',
    namespace: undefined,
    applications: [
      {
        code: 'crm',
        modules: ['contacts', 'prospecting', 'budgeting'],
        navigation: [
          { level: 'application', code: 'crm', label: 'CRM', icon: 'Building', iconType: 'Lucide', route: '/crm', displayOrder: 1 },
          { level: 'module', code: 'contacts', parentCode: 'crm', label: 'Contacts', icon: 'Users', iconType: 'Lucide', route: '/crm/contacts', displayOrder: 1 },
          { level: 'module', code: 'prospecting', parentCode: 'crm', label: 'Prospection', icon: 'Search', iconType: 'Lucide', route: '/crm/prospecting', displayOrder: 2 },
          { level: 'module', code: 'budgeting', parentCode: 'crm', label: 'Budget', icon: 'Wallet', iconType: 'Lucide', route: '/crm/budgeting', displayOrder: 3 },
          { level: 'section', code: 'directory', parentCode: 'contacts', label: 'Annuaire', icon: 'Circle', iconType: 'Lucide', route: '/crm/contacts/directory', displayOrder: 1 },
          { level: 'section', code: 'pipeline', parentCode: 'prospecting', label: 'Pipeline', icon: 'Circle', iconType: 'Lucide', route: '/crm/prospecting/pipeline', displayOrder: 1 },
          { level: 'section', code: 'overview', parentCode: 'budgeting', label: 'Vue d\'ensemble', icon: 'Circle', iconType: 'Lucide', route: '/crm/budgeting/overview', displayOrder: 1 },
        ],
        roles: [
          { code: 'admin', name: 'Administrator', applicationCode: 'crm', isDefault: false },
          { code: 'commercial', name: 'Commercial', applicationCode: 'crm', isDefault: true },
        ],
        permissions: [
          { path: 'crm.contacts.directory.access', action: 'access', sectionCode: 'directory' },
          { path: 'crm.contacts.directory.read', action: 'read', sectionCode: 'directory' },
          { path: 'crm.prospecting.pipeline.access', action: 'access', sectionCode: 'pipeline' },
          { path: 'crm.prospecting.pipeline.read', action: 'read', sectionCode: 'pipeline' },
          { path: 'crm.budgeting.overview.access', action: 'access', sectionCode: 'overview' },
          { path: 'crm.budgeting.overview.read', action: 'read', sectionCode: 'overview' },
        ],
        rolePermissions: [
          { roleCode: 'admin', permissionPath: 'crm.contacts.directory.access' },
          { roleCode: 'admin', permissionPath: 'crm.budgeting.overview.read' },
        ],
        testUsers: [{ roleCode: 'admin' }, { roleCode: 'commercial' }],
      },
    ],
    testUserPassword: 'DevTest!2026Secure',
    testUserEmailDomain: 'smartstack.local',
    emitDevCredentialsManifest: true,
    projectPath: '/tmp/fake',
  };
}

function fixtureTwoApps(): CoreSeedSpec {
  return {
    appCode: 'TestV2',
    namespace: undefined,
    applications: [
      {
        code: 'crm',
        modules: ['contacts'],
        navigation: [
          { level: 'application', code: 'crm', label: 'CRM', icon: 'Building', iconType: 'Lucide', route: '/crm', displayOrder: 1 },
          { level: 'module', code: 'contacts', parentCode: 'crm', label: 'Contacts', icon: 'Users', iconType: 'Lucide', route: '/crm/contacts', displayOrder: 1 },
          { level: 'section', code: 'directory', parentCode: 'contacts', label: 'Annuaire', icon: 'Circle', iconType: 'Lucide', route: '/crm/contacts/directory', displayOrder: 1 },
        ],
        roles: [{ code: 'admin', name: 'Administrator', applicationCode: 'crm', isDefault: false }],
        permissions: [
          { path: 'crm.contacts.directory.access', action: 'access', sectionCode: 'directory' },
          { path: 'crm.contacts.directory.read', action: 'read', sectionCode: 'directory' },
        ],
        rolePermissions: [],
        testUsers: [{ roleCode: 'admin' }],
      },
      {
        code: 'budgeting',
        modules: ['budget'],
        navigation: [
          { level: 'application', code: 'budgeting', label: 'Budget', icon: 'Wallet', iconType: 'Lucide', route: '/budgeting', displayOrder: 2 },
          { level: 'module', code: 'budget', parentCode: 'budgeting', label: 'Budget', icon: 'PieChart', iconType: 'Lucide', route: '/budgeting/budget', displayOrder: 1 },
          { level: 'section', code: 'overview', parentCode: 'budget', label: 'Vue d\'ensemble', icon: 'Circle', iconType: 'Lucide', route: '/budgeting/budget/overview', displayOrder: 1 },
        ],
        roles: [{ code: 'controller', name: 'Controller', applicationCode: 'budgeting', isDefault: false }],
        permissions: [
          { path: 'budgeting.budget.overview.access', action: 'access', sectionCode: 'overview' },
          { path: 'budgeting.budget.overview.read', action: 'read', sectionCode: 'overview' },
        ],
        rolePermissions: [],
        testUsers: [{ roleCode: 'controller' }],
      },
    ],
    testUserPassword: 'DevTest!2026Secure',
    testUserEmailDomain: 'smartstack.local',
    emitDevCredentialsManifest: true,
    projectPath: '/tmp/fake',
  };
}

describe('generate (scaffold-core-seed)', () => {
  it('produces 5 prefixed Core providers per application', () => {
    const result = generate(fixtureSingleApp());
    expect(result.files).toHaveLength(5);
    const fileNames = result.files.map((f) => f.path.split('/').pop()!);
    expect(fileNames).toEqual([
      'CrmCoreNavigationSeedDataProvider.cs',
      'CrmCoreRolesSeedDataProvider.cs',
      'CrmCorePermissionsSeedDataProvider.cs',
      'CrmCoreRolePermissionsSeedDataProvider.cs',
      'CrmCoreTestUsersSeedDataProvider.cs',
    ]);
  });

  it('places Core providers under Persistence/Seeding/Applications/{AppPascal}/Core/', () => {
    const result = generate(fixtureSingleApp());
    for (const file of result.files) {
      expect(file.path).toMatch(/^src\/TestV2\.Infrastructure\/Persistence\/Seeding\/Applications\/Crm\/Core\//);
    }
  });

  it('Core navigation provider includes every declared module — prevents Budget-empty regression', () => {
    const result = generate(fixtureSingleApp());
    const nav = result.files.find((f) => f.path.endsWith('CrmCoreNavigationSeedDataProvider.cs'))!;
    for (const moduleCode of ['contacts', 'prospecting', 'budgeting']) {
      expect(nav.content).toMatch(new RegExp(`NavigationModule\\.Create[\\s\\S]*code: "${moduleCode}"`));
    }
  });

  it('Roles provider resolves the parent application by code at runtime (no static GUID dep)', () => {
    const result = generate(fixtureSingleApp());
    const roles = result.files.find((f) => f.path.endsWith('CrmCoreRolesSeedDataProvider.cs'))!;
    expect(roles.content).toContain('Role.Create');
    expect(roles.content).toContain('NavigationApplications.FirstOrDefaultAsync');
    expect(roles.content).toContain('a.Code == "crm"');
  });

  it('Permissions provider emits Permission.CreateForSection lookup with ParseAction switch', () => {
    const result = generate(fixtureSingleApp());
    const perms = result.files.find((f) => f.path.endsWith('CrmCorePermissionsSeedDataProvider.cs'))!;
    // SmartStack 3.45: real seeding via Permission.CreateForSection (sectionId resolved
    // by code lookup at runtime), not a STUB.
    expect(perms.content).toContain('Permission.CreateForSection');
    expect(perms.content).toContain('NavigationSections.FirstOrDefaultAsync');
    expect(perms.content).toContain('private static PermissionAction? ParseAction');
    // Imports for SmartStack 3.45 Permission API.
    expect(perms.content).toContain('using SmartStack.Domain.Authorization;');
    expect(perms.content).toContain('using SmartStack.Domain.Navigation;');
    // The permissionsToSeed tuple array must contain every row from the spec.
    for (const path of [
      'crm.contacts.directory.access',
      'crm.contacts.directory.read',
      'crm.prospecting.pipeline.access',
      'crm.prospecting.pipeline.read',
      'crm.budgeting.overview.access',
      'crm.budgeting.overview.read',
    ]) {
      expect(perms.content).toContain(path);
    }
    // Idempotence guard on Path.
    expect(perms.content).toContain('p => p.Path == perm.Path');
  });

  it('TestUsers provider lists each test user as a TODO (User factory adapter required)', () => {
    const result = generate(fixtureSingleApp());
    const tu = result.files.find((f) => f.path.endsWith('CrmCoreTestUsersSeedDataProvider.cs'))!;
    // Stubbed: real seeding requires User.Create(email, passwordHash, …) with a hasher service.
    expect(tu.content).toContain('return Task.CompletedTask;');
    expect(tu.content).toContain('admin.test@smartstack.local');
    expect(tu.content).toContain('commercial.test@smartstack.local');
  });

  it('emits a test users manifest when emitDevCredentialsManifest=true', () => {
    const result = generate(fixtureSingleApp());
    expect(result.testUsersManifest).not.toBeNull();
    expect(result.testUsersManifest!.path).toBe('tests/ui-test/test-users.json');
    const parsed = JSON.parse(result.testUsersManifest!.content);
    expect(parsed.users).toHaveLength(2);
    expect(parsed.users[0]).toMatchObject({ role: 'admin', applicationCode: 'crm' });
  });

  it('omits the manifest when emitDevCredentialsManifest=false', () => {
    const spec = { ...fixtureSingleApp(), emitDevCredentialsManifest: false };
    const result = generate(spec);
    expect(result.testUsersManifest).toBeNull();
  });

  it('GUIDs are stable across runs (idempotence requirement)', () => {
    const a = generate(fixtureSingleApp());
    const b = generate(fixtureSingleApp());
    for (let i = 0; i < a.files.length; i++) {
      expect(a.files[i].content).toBe(b.files[i].content);
    }
  });

  it('exports the canonical 5-suffix list for the gate to reuse', () => {
    expect(CORE_PROVIDER_SUFFIXES).toEqual([
      'CoreNavigationSeedDataProvider',
      'CoreRolesSeedDataProvider',
      'CorePermissionsSeedDataProvider',
      'CoreRolePermissionsSeedDataProvider',
      'CoreTestUsersSeedDataProvider',
    ]);
    expect(coreProviderClassName('Crm', 'CoreNavigationSeedDataProvider')).toBe('CrmCoreNavigationSeedDataProvider');
  });
});

describe('generate (multi-app)', () => {
  it('produces 5 prefixed providers per app and places them in per-app folders', () => {
    const result = generate(fixtureTwoApps());
    expect(result.files).toHaveLength(10);

    const crmFiles = result.files.filter((f) => f.path.includes('/Applications/Crm/Core/'));
    const budgetingFiles = result.files.filter((f) => f.path.includes('/Applications/Budgeting/Core/'));
    expect(crmFiles).toHaveLength(5);
    expect(budgetingFiles).toHaveLength(5);

    expect(crmFiles.map((f) => f.path.split('/').pop()!)).toEqual([
      'CrmCoreNavigationSeedDataProvider.cs',
      'CrmCoreRolesSeedDataProvider.cs',
      'CrmCorePermissionsSeedDataProvider.cs',
      'CrmCoreRolePermissionsSeedDataProvider.cs',
      'CrmCoreTestUsersSeedDataProvider.cs',
    ]);
    expect(budgetingFiles.map((f) => f.path.split('/').pop()!)).toEqual([
      'BudgetingCoreNavigationSeedDataProvider.cs',
      'BudgetingCoreRolesSeedDataProvider.cs',
      'BudgetingCorePermissionsSeedDataProvider.cs',
      'BudgetingCoreRolePermissionsSeedDataProvider.cs',
      'BudgetingCoreTestUsersSeedDataProvider.cs',
    ]);
  });

  it('emits one DI registration block per application with distinct markers', () => {
    const result = generate(fixtureTwoApps());
    expect(result.diRegistration.perApp).toHaveLength(2);
    const codes = result.diRegistration.perApp.map((p) => p.appCode);
    expect(codes).toEqual(['crm', 'budgeting']);

    const crmBlock = result.diRegistration.perApp[0].markerBlock;
    expect(crmBlock).toContain(diBeginMarker('Crm'));
    expect(crmBlock).toContain(diEndMarker('Crm'));
    expect(crmBlock).toContain('CrmCoreNavigationSeedDataProvider');

    const budgetingBlock = result.diRegistration.perApp[1].markerBlock;
    expect(budgetingBlock).toContain(diBeginMarker('Budgeting'));
    expect(budgetingBlock).toContain(diEndMarker('Budgeting'));
    expect(budgetingBlock).toContain('BudgetingCoreNavigationSeedDataProvider');
  });

  it('consolidates test users from all apps into one manifest', () => {
    const result = generate(fixtureTwoApps());
    expect(result.testUsersManifest).not.toBeNull();
    const parsed = JSON.parse(result.testUsersManifest!.content);
    expect(parsed.users).toHaveLength(2);
    const apps = parsed.users.map((u: { applicationCode: string }) => u.applicationCode).sort();
    expect(apps).toEqual(['budgeting', 'crm']);
  });

  it('namespaces are scoped per app to keep prefixed class names readable', () => {
    const result = generate(fixtureTwoApps());
    const crmNav = result.files.find((f) => f.path.endsWith('CrmCoreNavigationSeedDataProvider.cs'))!;
    const budgetingNav = result.files.find((f) => f.path.endsWith('BudgetingCoreNavigationSeedDataProvider.cs'))!;
    expect(crmNav.content).toContain('namespace TestV2.Infrastructure.Persistence.Seeding.Applications.Crm.Core;');
    expect(budgetingNav.content).toContain('namespace TestV2.Infrastructure.Persistence.Seeding.Applications.Budgeting.Core;');
  });
});

describe('patchDependencyInjection', () => {
  function makeReg(appPascal: string) {
    return {
      appCode: appPascal.toLowerCase(),
      appPascal,
      markerBlock: `        ${diBeginMarker(appPascal)}\n        services.AddScoped<IClientSeedDataProvider, ${appPascal}CoreNavigationSeedDataProvider>();\n        ${diEndMarker(appPascal)}`,
    };
  }

  it('inserts the block before the last `return services;` when no markers exist', () => {
    const source = `namespace X;
public static class DI
{
    public static IServiceCollection AddInfrastructure(this IServiceCollection services)
    {
        services.AddDbContext<MyDb>();
        return services;
    }
}
`;
    const reg = makeReg('Crm');
    const next = patchDependencyInjection(source, reg);
    expect(next).not.toBeNull();
    expect(next!).toContain(diBeginMarker('Crm'));
    expect(next!).toContain(diEndMarker('Crm'));
    expect(next!.indexOf(diBeginMarker('Crm'))).toBeLessThan(next!.lastIndexOf('return services;'));
  });

  it('replaces only the matching app\'s block content idempotently', () => {
    const reg = makeReg('Crm');
    const source = `        ${diBeginMarker('Crm')}\n        services.AddScoped<IClientSeedDataProvider, OldProvider>();\n        ${diEndMarker('Crm')}\n`;
    const next = patchDependencyInjection(source, reg);
    expect(next).not.toBeNull();
    expect(next!).not.toContain('OldProvider');
    expect(next!).toContain('CrmCoreNavigationSeedDataProvider');
  });

  it('returns null when source already matches the block (no-op)', () => {
    const reg = makeReg('Crm');
    const source = `prefix\n${reg.markerBlock}\nsuffix`;
    const next = patchDependencyInjection(source, reg);
    expect(next).toBeNull();
  });

  it('patches independently when 2 apps are sequenced — each app finds its own markers', () => {
    const crm = makeReg('Crm');
    const budgeting = makeReg('Budgeting');
    let source = `namespace X;
public static class DI
{
    public static IServiceCollection AddInfrastructure(this IServiceCollection services)
    {
        return services;
    }
}
`;
    const afterCrm = patchDependencyInjection(source, crm)!;
    expect(afterCrm).toContain(diBeginMarker('Crm'));
    expect(afterCrm).not.toContain(diBeginMarker('Budgeting'));
    const afterBoth = patchDependencyInjection(afterCrm, budgeting)!;
    expect(afterBoth).toContain(diBeginMarker('Crm'));
    expect(afterBoth).toContain(diBeginMarker('Budgeting'));
  });
});
