"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var ModuleTestDataService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModuleTestDataService = void 0;
const common_1 = require("@nestjs/common");
const core_1 = require("@nestjs/core");
const typeorm_1 = require("@nestjs/typeorm");
const string_helper_1 = require("../helpers/string.helper");
const typeorm_2 = require("typeorm");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const solid_core_metadata_json_1 = __importDefault(require("./seed-data/solid-core-metadata.json"));
const create_media_storage_provider_metadata_dto_1 = require("../dtos/create-media-storage-provider-metadata.dto");
const module_helper_1 = require("../helpers/module.helper");
const solid_registry_1 = require("../helpers/solid-registry");
const media_repository_1 = require("../repository/media.repository");
const permission_metadata_repository_1 = require("../repository/permission-metadata.repository");
const authentication_service_1 = require("../services/authentication.service");
const model_metadata_service_1 = require("../services/model-metadata.service");
const role_metadata_service_1 = require("../services/role-metadata.service");
const user_service_1 = require("../services/user.service");
const mediaStorageProviders_1 = require("../services/mediaStorageProviders");
let ModuleTestDataService = ModuleTestDataService_1 = class ModuleTestDataService {
    constructor(moduleRef, discoveryService, solidRegistry) {
        this.moduleRef = moduleRef;
        this.discoveryService = discoveryService;
        this.solidRegistry = solidRegistry;
        this.logger = new common_1.Logger(ModuleTestDataService_1.name);
    }
    async setupTestData(modulesToTest) {
        const testDataFiles = this.testDataFiles;
        const filteredFiles = modulesToTest?.length ? testDataFiles.filter((file) => modulesToTest.includes(file.moduleMetadata?.name)) : testDataFiles;
        if (filteredFiles.length === 0) {
            this.logger.warn('No modules matched the provided modulesToTest list.');
            console.log('No modules matched the provided modulesToTest list.');
            return;
        }
        for (const overallMetadata of filteredFiles) {
            const moduleName = overallMetadata?.moduleMetadata?.name ?? 'unknown';
            this.logger.log(`Processing test data for module: ${moduleName}`);
            console.log(`Processing test data for module: ${moduleName}`);
            await this.seedTestData(overallMetadata);
            console.log(`✔ Test data setup complete for module: ${moduleName}`);
        }
    }
    async removeTestData(modulesToTest) {
        const testDataFiles = this.testDataFiles;
        const filteredFiles = modulesToTest?.length ? testDataFiles.filter((file) => modulesToTest.includes(file.moduleMetadata?.name)) : testDataFiles;
        if (filteredFiles.length === 0) {
            this.logger.warn('No modules matched the provided modulesToTest list.');
            console.log('No modules matched the provided modulesToTest list.');
            return;
        }
        for (const overallMetadata of filteredFiles) {
            const moduleName = overallMetadata?.moduleMetadata?.name ?? 'unknown';
            this.logger.log(`Removing test data for module: ${moduleName}`);
            console.log(`Removing test data for module: ${moduleName}`);
            await this.unlinkTestData(overallMetadata);
            console.log(`✔ Test data unlink complete for module: ${moduleName}`);
        }
    }
    async createTestDatasources() {
        const manifestPath = path.join(process.cwd(), '.solidx-test-manifest');
        if (fs.existsSync(manifestPath)) {
            console.log('Existing .solidx-test-manifest found; skipping test datasource creation.');
            return;
        }
        const dbRunName = this.generateDbRunName();
        const timestamp = this.getTimestamp();
        const envPath = path.join(process.cwd(), '.env');
        if (!fs.existsSync(envPath)) {
            throw new Error(`Base .env file not found at ${envPath}`);
        }
        const datasourceNames = this.solidRegistry
            .getSolidDatabaseModules()
            .map((wrapper) => wrapper.instance?.name?.())
            .filter(Boolean)
            .map((name) => name.toLowerCase());
        if (datasourceNames.length === 0) {
            throw new Error('No solid database modules registered; cannot create test datasources.');
        }
        const dbNameByDatasource = new Map();
        for (const dsName of datasourceNames) {
            dbNameByDatasource.set(dsName, `${dsName}_${timestamp}_${dbRunName}`);
        }
        const newEnvContents = this.buildTestEnvContents(fs.readFileSync(envPath, 'utf-8'), dbNameByDatasource);
        const backupEnvPath = path.join(process.cwd(), `.env.backup.${dbRunName}`);
        fs.copyFileSync(envPath, backupEnvPath);
        fs.writeFileSync(envPath, newEnvContents);
        console.log(`Backed up .env to ${path.basename(backupEnvPath)} and applied new test datasource names to .env.`);
        this.updateTestManifest(dbRunName, timestamp, dbNameByDatasource);
        await this.createTestDatabaseObjects(dbNameByDatasource);
        const dbList = Array.from(dbNameByDatasource.entries())
            .map(([dsName, dbName]) => `- ${dsName}: ${dbName}`)
            .join('\n');
        const instructions = [
            '',
            '============================================================',
            '  TEST DATASOURCE ENVIRONMENT CREATED',
            '------------------------------------------------------------',
            `  Run name : ${dbRunName}`,
            `  Env backup : ${path.basename(backupEnvPath)}`,
            '',
            '  Test databases/schemas created:',
            dbList || '  (none)',
            '',
            '  Next steps:',
            '  1) Using updated .env with test datasource names',
            '  2) Run solid seed as usual',
            '  3) Proceed with the next steps in the workflow',
            '============================================================',
            '',
        ].join('\n');
        console.log(instructions);
    }
    async deleteTestDatasources() {
        const manifestPath = path.join(process.cwd(), '.solidx-test-manifest');
        if (!fs.existsSync(manifestPath)) {
            this.logger.log('No .solidx-test-manifest found; nothing to delete.');
            console.log('No .solidx-test-manifest found; nothing to delete.');
            return;
        }
        const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
        const runs = manifest?.runs ?? {};
        const runNames = Object.keys(runs);
        if (runNames.length === 0) {
            fs.unlinkSync(manifestPath);
            return;
        }
        const latestRunName = runNames
            .slice()
            .sort((a, b) => {
            const aCreated = runs[a]?.createdAt ?? '';
            const bCreated = runs[b]?.createdAt ?? '';
            return aCreated.localeCompare(bCreated);
        })
            .pop();
        if (latestRunName) {
            const backupEnvPath = path.join(process.cwd(), `.env.backup.${latestRunName}`);
            if (fs.existsSync(backupEnvPath)) {
                fs.copyFileSync(backupEnvPath, path.join(process.cwd(), '.env'));
                console.log(`Restored .env from ${path.basename(backupEnvPath)}.`);
                fs.unlinkSync(backupEnvPath);
            }
        }
        for (const runName of runNames) {
            const envFileName = `.env.${runName}`;
            const envPath = path.join(process.cwd(), envFileName);
            if (fs.existsSync(envPath)) {
                fs.unlinkSync(envPath);
            }
        }
        for (const runName of runNames) {
            const databases = runs[runName]?.databases ?? {};
            await this.dropTestDatabaseObjects(databases);
        }
        fs.unlinkSync(manifestPath);
        console.log('✔ Test datasource env files and manifest deleted; test databases dropped.');
    }
    get testDataFiles() {
        const typedSolidCoreMetadata = structuredClone(solid_core_metadata_json_1.default);
        const testDataFiles = [typedSolidCoreMetadata];
        const enabledModules = (0, module_helper_1.getDynamicModuleNamesBasedOnMetadata)();
        for (const enabledModule of enabledModules) {
            const enabledModuleSeedFile = `module-metadata/${enabledModule}/${enabledModule}-metadata.json`;
            const fullPath = path.join(process.cwd(), enabledModuleSeedFile);
            if (fs.existsSync(fullPath)) {
                const overallMetadata = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
                testDataFiles.push(overallMetadata);
            }
        }
        return testDataFiles;
    }
    async seedTestData(overallMetadata) {
        const moduleMetadata = overallMetadata.moduleMetadata;
        if (!moduleMetadata) {
            throw new Error('Module metadata missing from test data payload.');
        }
        const testingRoles = overallMetadata?.testing?.roles ?? [];
        const testingUsers = overallMetadata?.testing?.users ?? [];
        const testingData = overallMetadata?.testing?.data ?? [];
        if (testingRoles.length > 0) {
            await this.seedTestRoles(testingRoles);
        }
        if (testingUsers.length > 0) {
            await this.seedTestUsers(testingUsers);
        }
        if (testingData.length === 0) {
            this.logger.debug(`No test data found for ${moduleMetadata.name}`);
            return;
        }
        const modelsByName = new Map((moduleMetadata.models ?? []).map((m) => [m.singularName, m]));
        for (const entry of testingData) {
            const modelUserKey = entry.modelUserKey;
            const modelDef = modelsByName.get(modelUserKey);
            if (!modelDef) {
                throw new Error(`Test data modelUserKey not found in metadata: ${modelUserKey}`);
            }
            const entityRepo = this.resolveRepository(modelUserKey);
            const payload = { ...(entry.data ?? {}) };
            for (const field of modelDef.fields ?? []) {
                if (field.type === 'relation' && field.relationType === 'many-to-one') {
                    const userKeyProp = `${field.name}UserKey`;
                    if (!(userKeyProp in payload)) {
                        continue;
                    }
                    const userKeyValue = payload[userKeyProp];
                    if (userKeyValue === null || userKeyValue === undefined || userKeyValue === '') {
                        delete payload[userKeyProp];
                        continue;
                    }
                    const coModelName = field.relationCoModelSingularName;
                    const coModelDef = coModelName ? modelsByName.get(coModelName) : null;
                    if (!coModelDef) {
                        throw new Error(`Test data relation model ${coModelName} not found in metadata, when attempting to resolve field ${modelDef.singularName}.${field.name}`);
                    }
                    const coUserKeyField = coModelDef.userKeyFieldUserKey;
                    if (!coUserKeyField) {
                        throw new Error(`Test data relation model ${coModelName} is missing userKeyFieldUserKey, when attempting to resolve field ${modelDef.singularName}.${field.name}`);
                    }
                    const coRepo = this.resolveRepository(coModelName);
                    const related = typeof coRepo.findOneByUserKey === 'function'
                        ? await coRepo.findOneByUserKey(userKeyValue)
                        : await coRepo.findOne({ where: { [coUserKeyField]: userKeyValue } });
                    if (!related) {
                        throw new Error(`Test data relation not found: ${coModelName}.${coUserKeyField}=${userKeyValue}`);
                    }
                    payload[field.name] = related;
                    delete payload[userKeyProp];
                }
            }
            const mediaPayload = {};
            for (const field of modelDef.fields ?? []) {
                if ((field.type === 'mediaSingle' || field.type === 'mediaMultiple') && payload[field.name] !== undefined) {
                    mediaPayload[field.name] = payload[field.name];
                    delete payload[field.name];
                }
            }
            const multiRelationPayload = [];
            for (const field of modelDef.fields ?? []) {
                if (field.type !== 'relation')
                    continue;
                if (field.relationType !== 'many-to-many' && field.relationType !== 'one-to-many')
                    continue;
                const userKeysProp = `${field.name}UserKeys`;
                if (userKeysProp in payload && Array.isArray(payload[userKeysProp])) {
                    multiRelationPayload.push({ field, userKeys: payload[userKeysProp] });
                    delete payload[userKeysProp];
                }
                delete payload[field.name];
            }
            let savedEntity;
            const userKeyField = modelDef.userKeyFieldUserKey;
            const recordUserKeyValue = entry.recUserKeyValue ?? payload[userKeyField];
            const recordLabel = `${modelUserKey}.${userKeyField}=${recordUserKeyValue}`;
            if (userKeyField && payload[userKeyField] !== undefined) {
                const existing = await entityRepo.findOne({
                    where: { [userKeyField]: payload[userKeyField] },
                });
                if (existing) {
                    console.log(`Updating test data record: ${recordLabel}`);
                    savedEntity = await entityRepo.save(entityRepo.merge(existing, payload));
                }
                else {
                    console.log(`Creating test data record: ${recordLabel}`);
                    savedEntity = await entityRepo.save(entityRepo.create(payload));
                }
            }
            else {
                console.log(`Creating test data record without user key lookup: ${modelUserKey}`);
                savedEntity = await entityRepo.save(entityRepo.create(payload));
            }
            if (multiRelationPayload.length > 0) {
                await this.seedMultiRelations(savedEntity.id, modelUserKey, multiRelationPayload, modelsByName);
            }
            if (Object.keys(mediaPayload).length > 0) {
                await this.seedEntityMedia(savedEntity.id, modelUserKey, mediaPayload);
            }
        }
    }
    async unlinkTestData(overallMetadata) {
        const moduleMetadata = overallMetadata.moduleMetadata;
        if (!moduleMetadata) {
            throw new Error('Module metadata missing from test data payload.');
        }
        const testingData = overallMetadata?.testing?.data ?? [];
        if (testingData.length === 0) {
            this.logger.debug(`No test data found for ${moduleMetadata.name}`);
            return;
        }
        const modelsByName = new Map((moduleMetadata.models ?? []).map((m) => [m.singularName, m]));
        for (const entry of [...testingData].reverse()) {
            const modelUserKey = entry.modelUserKey;
            const modelDef = modelsByName.get(modelUserKey);
            if (!modelDef) {
                throw new Error(`Test data modelUserKey not found in metadata: ${modelUserKey}`);
            }
            const userKeyField = modelDef.userKeyFieldUserKey;
            if (!userKeyField) {
                throw new Error(`Cannot unlink test data for model ${modelUserKey} because userKeyFieldUserKey is not configured.`);
            }
            const userKeyValue = entry.data?.[userKeyField];
            if (userKeyValue === undefined || userKeyValue === null || userKeyValue === '') {
                throw new Error(`Cannot unlink test data for model ${modelUserKey} because testing.data entry is missing the actual user key field "${userKeyField}" in data.`);
            }
            const recordLabel = `${modelUserKey}.${userKeyField}=${userKeyValue}`;
            const entityRepo = this.resolveRepository(modelUserKey);
            const existing = typeof entityRepo.findOneByUserKey === 'function'
                ? await entityRepo.findOneByUserKey(userKeyValue)
                : await entityRepo.findOne({ where: { [userKeyField]: userKeyValue } });
            if (!existing) {
                console.log(`Skipping unlink; test data record not found: ${recordLabel}`);
                this.logger.debug(`Test data record not found for unlink: ${modelUserKey}.${userKeyField}=${userKeyValue}`);
                continue;
            }
            console.log(`Deleting test data record: ${recordLabel}`);
            await entityRepo.remove(existing);
            this.logger.debug(`Removed test data record: ${modelUserKey}.${userKeyField}=${userKeyValue}`);
        }
    }
    async seedTestRoles(roles) {
        const roleService = this.moduleRef.get(role_metadata_service_1.RoleMetadataService, { strict: false });
        if (!roleService) {
            throw new Error('RoleMetadataService not available — cannot seed test roles.');
        }
        await roleService.createRolesIfNotExists(roles.map((r) => ({ name: r.name })));
        for (const role of roles) {
            const perms = role.permissions ?? [];
            if (perms.length === 0)
                continue;
            if (perms.some((p) => p === '*')) {
                await roleService.addAllPermissionsToRole(role.name);
                this.logger.log(`Bound all permissions to test role "${role.name}"`);
                continue;
            }
            const expanded = await this.expandPermissionNames(perms);
            if (expanded.length === 0) {
                this.logger.warn(`Test role "${role.name}" has permissions declared but none resolved — skipping binding.`);
                continue;
            }
            try {
                await roleService.addPermissionsToRole(role.name, expanded);
            }
            catch (err) {
                throw new Error(`Failed to bind permissions to test role "${role.name}": ${err?.message ?? err}. ` +
                    `Did you run "solid seed" first so controller permissions are registered?`);
            }
            this.logger.log(`Bound ${expanded.length} permissions to test role "${role.name}"`);
        }
    }
    async expandPermissionNames(permissions) {
        const permissionRepo = this.moduleRef.get(permission_metadata_repository_1.PermissionMetadataRepository, { strict: false });
        if (!permissionRepo) {
            throw new Error('PermissionMetadataRepository not available — cannot resolve test role permissions.');
        }
        const exact = new Set();
        const prefixes = [];
        for (const entry of permissions) {
            if (!entry)
                continue;
            if (entry.endsWith('.*')) {
                prefixes.push(entry.slice(0, -1));
            }
            else {
                exact.add(entry);
            }
        }
        if (prefixes.length > 0) {
            const allPermissions = await permissionRepo.find();
            for (const p of allPermissions) {
                if (prefixes.some((prefix) => p.name.startsWith(prefix))) {
                    exact.add(p.name);
                }
            }
        }
        return Array.from(exact);
    }
    async seedTestUsers(users) {
        const userService = this.moduleRef.get(user_service_1.UserService, { strict: false });
        const authService = this.moduleRef.get(authentication_service_1.AuthenticationService, { strict: false });
        if (!userService || !authService) {
            throw new Error('UserService / AuthenticationService not available — cannot seed test users.');
        }
        for (const user of users) {
            const existing = await userService.findOneByUsername(user.username);
            if (existing) {
                this.logger.debug(`Test user "${user.username}" already exists — skipping signUp.`);
                continue;
            }
            await authService.signUp({ ...user });
            this.logger.log(`Created test user "${user.username}"${user.roles?.length ? ` with roles [${user.roles.join(', ')}]` : ''}`);
        }
    }
    async seedMultiRelations(entityId, modelUserKey, relations, modelsByName) {
        for (const { field, userKeys } of relations) {
            if (!userKeys.length)
                continue;
            const coModelName = field.relationCoModelSingularName;
            const coModelDef = modelsByName.get(coModelName);
            if (!coModelDef) {
                throw new Error(`Relation model "${coModelName}" not found in metadata for field ${modelUserKey}.${field.name}`);
            }
            const coUserKeyField = coModelDef.userKeyFieldUserKey;
            if (!coUserKeyField) {
                throw new Error(`Relation model "${coModelName}" is missing userKeyFieldUserKey, needed to resolve ${modelUserKey}.${field.name}`);
            }
            const coRepo = this.resolveRepository(coModelName);
            const resolvedIds = [];
            for (const uk of userKeys) {
                const related = typeof coRepo.findOneByUserKey === 'function'
                    ? await coRepo.findOneByUserKey(uk)
                    : await coRepo.findOne({ where: { [coUserKeyField]: uk } });
                if (!related) {
                    throw new Error(`Related entity not found: ${coModelName}.${coUserKeyField}=${uk}`);
                }
                resolvedIds.push(related.id);
            }
            const existingRelated = await this.entityManager
                .createQueryBuilder()
                .relation((0, string_helper_1.classify)(modelUserKey), field.name)
                .of(entityId)
                .loadMany();
            const existingIds = existingRelated.map((e) => e.id);
            const toAdd = resolvedIds.filter((id) => !existingIds.includes(id));
            const toRemove = existingIds.filter((id) => !resolvedIds.includes(id));
            if (toAdd.length > 0 || toRemove.length > 0) {
                await this.entityManager
                    .createQueryBuilder()
                    .relation((0, string_helper_1.classify)(modelUserKey), field.name)
                    .of(entityId)
                    .addAndRemove(toAdd, toRemove);
            }
            this.logger.debug(`Seeded ${field.relationType} relation ${modelUserKey}.${field.name} entityId=${entityId}: +${toAdd.length} -${toRemove.length}`);
        }
    }
    async seedEntityMedia(entityId, modelUserKey, mediaPayload) {
        const mediaBasePath = process.env.TEST_UPLOADS_MEDIA_FILE_PATH;
        if (!mediaBasePath) {
            throw new Error('TEST_UPLOADS_MEDIA_FILE_PATH is not set. Cannot seed test media.');
        }
        const modelMetadata = await this.modelMetadataService.findOneBySingularName(modelUserKey, {
            fields: {
                model: { userKeyField: true },
                mediaStorageProvider: true,
            },
        });
        for (const [fieldName, fileName] of Object.entries(mediaPayload)) {
            if (!fileName)
                continue;
            const fieldMetadata = modelMetadata.fields.find((f) => f.name === fieldName);
            if (!fieldMetadata) {
                throw new Error(`Media field "${fieldName}" not found in loaded metadata for model ${modelUserKey}`);
            }
            if (!fieldMetadata.mediaStorageProvider) {
                throw new Error(`Media field "${fieldName}" in model ${modelUserKey} has no storage provider configured`);
            }
            const storageProviderType = fieldMetadata.mediaStorageProvider.type;
            if (storageProviderType !== create_media_storage_provider_metadata_dto_1.MediaStorageProviderType.Filesystem) {
                throw new Error(`Test media seeding supports filesystem storage only. Field "${fieldName}" uses "${storageProviderType}".`);
            }
            const existing = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(entityId, fieldMetadata.id, fieldMetadata.model.id);
            if (existing.length > 0) {
                this.logger.debug(`Media already seeded for ${modelUserKey}.${fieldName} entityId=${entityId}, skipping`);
                continue;
            }
            const sourcePath = path.join(mediaBasePath, fileName);
            if (!fs.existsSync(sourcePath)) {
                throw new Error(`Test media file not found: ${sourcePath}`);
            }
            const storageProvider = await (0, mediaStorageProviders_1.getMediaStorageProvider)(this.moduleRef, storageProviderType);
            const stream = fs.createReadStream(sourcePath);
            await storageProvider.storeStreams([[stream, fileName]], { id: entityId }, fieldMetadata);
            this.logger.debug(`Seeded media for ${modelUserKey}.${fieldName} entityId=${entityId} file=${fileName}`);
        }
    }
    get entityManager() {
        return this.moduleRef.get(typeorm_2.EntityManager, { strict: false });
    }
    get modelMetadataService() {
        return this.moduleRef.get(model_metadata_service_1.ModelMetadataService, { strict: false });
    }
    get mediaRepository() {
        return this.moduleRef.get(media_repository_1.MediaRepository, { strict: false });
    }
    resolveRepository(modelUserKey) {
        const repoName = `${(0, string_helper_1.classify)(modelUserKey)}Repository`;
        const providers = this.discoveryService.getProviders();
        const wrapper = providers.find((provider) => provider.name === repoName);
        const repo = wrapper?.instance;
        if (repo) {
            return repo;
        }
        try {
            const resolved = this.moduleRef.get(repoName, { strict: false });
            if (resolved) {
                return resolved;
            }
        }
        catch {
        }
        throw new Error(`Repository not found for model ${modelUserKey}. Expected provider: ${repoName}`);
    }
    generateDbRunName() {
        const adjectives = [
            'brave', 'bright', 'calm', 'clever', 'curious', 'gentle', 'jolly', 'lively', 'mighty', 'nimble',
            'proud', 'quick', 'quiet', 'sharp', 'sly', 'steady', 'swift', 'wise', 'witty', 'zesty',
        ];
        const animals = [
            'lion', 'tiger', 'panther', 'eagle', 'falcon', 'otter', 'wolf', 'fox', 'bear', 'badger',
            'monkey', 'panda', 'leopard', 'whale', 'dolphin', 'rhino', 'giraffe', 'camel', 'koala', 'lynx',
        ];
        const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
        const animal = animals[Math.floor(Math.random() * animals.length)];
        return `${adjective}_${animal}`;
    }
    getTimestamp() {
        const now = new Date();
        const pad = (value) => value.toString().padStart(2, '0');
        return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
    }
    buildTestEnvContents(baseEnv, dbNameByDatasource) {
        const datasourceNameSet = new Set(Array.from(dbNameByDatasource.keys()));
        const lines = baseEnv.split(/\r?\n/);
        return lines
            .map((line) => {
            const trimmed = line.trim();
            if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
                return line;
            }
            const [rawKey] = line.split('=');
            const key = rawKey.trim();
            if (!key.endsWith('_DATABASE_NAME')) {
                return line;
            }
            const prefix = key.replace(/_DATABASE_NAME$/, '').toLowerCase();
            const matchedDatasource = Array.from(datasourceNameSet).find((name) => name === prefix);
            if (!matchedDatasource) {
                return line;
            }
            const dbName = dbNameByDatasource.get(matchedDatasource);
            if (!dbName) {
                return line;
            }
            return `${key}=${dbName}`;
        })
            .join('\n');
    }
    async createTestDatabaseObjects(dbNameByDatasource) {
        for (const [dsName, dbName] of dbNameByDatasource.entries()) {
            const dataSource = this.resolveDataSourceByName(dsName);
            if (!dataSource.isInitialized) {
                await dataSource.initialize();
            }
            console.log(`Creating test database/schema "${dbName}" on datasource "${dsName}"...`);
            const queryRunner = dataSource.createQueryRunner();
            try {
                const type = dataSource.options.type;
                if (type === 'postgres') {
                    await queryRunner.query(`CREATE DATABASE "${dbName}"`);
                }
                else if (type === 'mssql') {
                    await queryRunner.query(`IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${dbName}') EXEC('CREATE SCHEMA [${dbName}]')`);
                }
                else if (type === 'mysql' || type === 'mariadb') {
                    await queryRunner.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
                }
                else {
                    throw new Error(`Unsupported database type for test data creation: ${type}`);
                }
            }
            finally {
                await queryRunner.release();
            }
        }
    }
    resolveDataSourceByName(datasourceName) {
        const token = datasourceName ? (0, typeorm_1.getDataSourceToken)(datasourceName) : (0, typeorm_1.getDataSourceToken)();
        try {
            const ds = this.moduleRef.get(token, { strict: false });
            if (!ds) {
                throw new Error(`No DataSource found for "${datasourceName}"`);
            }
            return ds;
        }
        catch (err) {
            throw new Error(`Failed to resolve DataSource "${datasourceName}": ${err?.message ?? err}`);
        }
    }
    updateTestManifest(dbRunName, timestamp, dbNameByDatasource) {
        const manifestPath = path.join(process.cwd(), '.solidx-test-manifest');
        let manifest = {};
        if (fs.existsSync(manifestPath)) {
            try {
                const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
                if (parsed && typeof parsed === 'object') {
                    manifest = parsed;
                }
            }
            catch {
            }
        }
        if (!manifest.runs || typeof manifest.runs !== 'object') {
            manifest.runs = {};
        }
        const databases = {};
        for (const [dsName, dbName] of dbNameByDatasource.entries()) {
            databases[dsName] = dbName;
        }
        manifest.runs[dbRunName] = {
            createdAt: timestamp,
            databases,
        };
        fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
    }
    async dropTestDatabaseObjects(databases) {
        const entries = Object.entries(databases);
        for (const [dsName, dbName] of entries) {
            await this.dropTestDatabaseObjectsWithRetry(dsName, dbName);
        }
    }
    async dropTestDatabaseObjectsWithRetry(dsName, dbName) {
        let lastError;
        for (let attempt = 1; attempt <= ModuleTestDataService_1.TEARDOWN_RETRY_ATTEMPTS; attempt += 1) {
            console.log(`Attempting to tear down "${dbName}" on datasource "${dsName}" (${attempt}/${ModuleTestDataService_1.TEARDOWN_RETRY_ATTEMPTS})...`);
            try {
                await this.dropSingleTestDatabaseObject(dsName, dbName);
                return;
            }
            catch (error) {
                lastError = error;
                if (attempt >= ModuleTestDataService_1.TEARDOWN_RETRY_ATTEMPTS) {
                    throw error;
                }
                await this.sleep(this.teardownRetryDelayMs(attempt));
            }
        }
        throw lastError instanceof Error ? lastError : new Error(String(lastError));
    }
    teardownRetryDelayMs(attempt) {
        const baseMs = 500;
        const incrementMs = 350;
        const jitterMs = Math.floor(Math.random() * 250);
        return baseMs + ((attempt - 1) * incrementMs) + jitterMs;
    }
    async sleep(ms) {
        await new Promise((resolve) => setTimeout(resolve, ms));
    }
    async dropSingleTestDatabaseObject(dsName, dbName) {
        const dataSource = this.resolveDataSourceByName(dsName);
        if (dataSource.options.type === 'postgres') {
            await this.dropPostgresDatabase(dataSource, dbName);
            return;
        }
        if (!dataSource.isInitialized) {
            await dataSource.initialize();
        }
        const queryRunner = dataSource.createQueryRunner();
        try {
            const type = dataSource.options.type;
            if (type === 'mssql') {
                await this.dropMssqlSchema(queryRunner, dbName);
            }
            else if (type === 'mysql' || type === 'mariadb') {
                await queryRunner.query(`DROP DATABASE IF EXISTS \`${dbName}\``);
            }
            else {
                throw new Error(`Unsupported database type for test data deletion: ${type}`);
            }
        }
        finally {
            await queryRunner.release();
        }
    }
    async dropPostgresDatabase(dataSource, dbName) {
        if (dataSource.isInitialized) {
            await dataSource.destroy();
        }
        const adminDataSource = new typeorm_2.DataSource({
            ...dataSource.options,
            database: this.resolvePostgresMaintenanceDatabase(dataSource),
            name: `${String(dataSource.name ?? 'default')}_teardown_admin_${Date.now()}`,
            synchronize: false,
            migrationsRun: false,
            entities: [],
            subscribers: [],
            migrations: [],
        });
        try {
            await adminDataSource.initialize();
            const queryRunner = adminDataSource.createQueryRunner();
            try {
                await queryRunner.query(`SELECT pg_terminate_backend(pid)
             FROM pg_stat_activity
            WHERE datname = $1
              AND pid <> pg_backend_pid()`, [dbName]);
                await queryRunner.query(`DROP DATABASE IF EXISTS "${dbName}"`);
            }
            finally {
                await queryRunner.release();
            }
        }
        finally {
            if (adminDataSource.isInitialized) {
                await adminDataSource.destroy();
            }
        }
    }
    resolvePostgresMaintenanceDatabase(dataSource) {
        const configured = process.env.POSTGRES_MAINTENANCE_DATABASE?.trim();
        if (configured) {
            return configured;
        }
        const currentDb = String(dataSource.options?.database ?? '').trim();
        if (currentDb && currentDb !== 'postgres') {
            return 'postgres';
        }
        return 'template1';
    }
    async dropMssqlSchema(queryRunner, schemaName) {
        const foreignKeys = await queryRunner.query(`SELECT fk.name AS fk_name, t.name AS table_name
       FROM sys.foreign_keys fk
       INNER JOIN sys.tables t ON fk.parent_object_id = t.object_id
       INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
       WHERE s.name = '${schemaName}'`);
        for (const fk of foreignKeys) {
            await queryRunner.query(`ALTER TABLE [${schemaName}].[${fk.table_name}] DROP CONSTRAINT [${fk.fk_name}]`);
        }
        const tables = await queryRunner.query(`SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '${schemaName}'`);
        for (const table of tables) {
            await queryRunner.query(`DROP TABLE [${schemaName}].[${table.TABLE_NAME}]`);
        }
        const views = await queryRunner.query(`SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_SCHEMA = '${schemaName}'`);
        for (const view of views) {
            await queryRunner.query(`DROP VIEW [${schemaName}].[${view.TABLE_NAME}]`);
        }
        await queryRunner.query(`DROP SCHEMA [${schemaName}]`);
    }
};
exports.ModuleTestDataService = ModuleTestDataService;
ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS = 5;
exports.ModuleTestDataService = ModuleTestDataService = ModuleTestDataService_1 = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [core_1.ModuleRef,
        core_1.DiscoveryService,
        solid_registry_1.SolidRegistry])
], ModuleTestDataService);
//# sourceMappingURL=module-test-data.service.js.map