/**
 * Fixture insertion logic with ordered insertion and rollback support
 *
 * Inserts fixture data into the database in the correct order to satisfy
 * foreign key constraints, with savepoint-based rollback for detailed error messages.
 *
 * @see packages/server/src/__tests__/helpers/fixture-types.ts for fixture structure
 * @see packages/server/src/__tests__/helpers/fixture-validation.ts for validation logic
 */
import { assertValidFixture } from './fixture-validation.js';
import { qualifyTable } from './test-db-config.js';
import { makeUUID, makeUUIDLegacy } from '../../demo-fixtures/helpers/deterministic-uuid.js';
import { UUID_REGEX } from '../../shared/constants.js';
import { ensureBusinessForEntity, ensureFinancialEntity, ensureTaxCategoryForEntity } from './seed-helpers.js';
/**
 * Custom error for fixture insertion failures
 *
 * Provides context about which section of the fixture failed to insert
 * and includes the underlying database error.
 */
export class FixtureInsertionError extends Error {
    section;
    originalError;
    context;
    constructor(section, originalError, context) {
        super(`Failed to insert fixture section "${section}": ${originalError.message}` +
            (context ? `\nContext: ${JSON.stringify(context, null, 2)}` : ''));
        this.section = section;
        this.originalError = originalError;
        this.context = context;
        this.name = 'FixtureInsertionError';
        Error.captureStackTrace?.(this, FixtureInsertionError);
    }
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            section: this.section,
            originalError: {
                name: this.originalError.name,
                message: this.originalError.message,
            },
            context: this.context,
        };
    }
}
/**
 * Insert a complete fixture into the database
 *
 * Performs ordered insertion of all fixture entities with savepoint-based
 * error handling for clear diagnostics. Insertion order ensures foreign key
 * constraints are satisfied:
 *
 * 1. Businesses (financial entities)
 * 2. Tax categories (financial entities)
 * 3. Financial accounts (references businesses)
 * 4. Charges (references businesses and tax categories)
 * 5. Transactions (references charges, accounts, businesses)
 * 6. Documents (references charges and businesses)
 *
 * Each section is wrapped in a SAVEPOINT so that partial failures can be
 * rolled back to a known state with clear error context.
 *
 * **Transaction Handling:**
 * - Must be called within an existing transaction (client has active BEGIN)
 * - Creates savepoints for each section (SAVEPOINT section_name)
 * - On section error, rolls back to savepoint (ROLLBACK TO SAVEPOINT section_name)
 * - Does NOT commit or rollback the outer transaction
 *
 * **Validation:**
 * - Validates fixture before any insertion (assertValidFixture)
 * - Checks referential integrity (charge IDs, business IDs, etc.)
 * - Validates required fields
 *
 * @param client - PostgreSQL client (PoolClient or standalone Client) within an active transaction
 * @param fixture - Complete fixture to insert
 * @returns Promise resolving to ID mapping (fixture ID → database ID)
 * @throws {Error} If fixture validation fails (via assertValidFixture)
 * @throws {FixtureInsertionError} If any insertion section fails
 *
 * @remarks
 * Type Safety: Accepts both PoolClient and Client to support both test transactions
 * (pool.connect()) and standalone connections (new pg.Client()) used in seed scripts.
 *
 * @example
 * ```typescript
 * import { withTestTransaction } from './test-transaction.js';
 * import { insertFixture } from './fixture-loader.js';
 * import { expenseScenarioA } from '../fixtures/expenses/expense-scenario-a.js';
 *
 * it('should insert expense scenario', () =>
 *   withTestTransaction(pool, async (client) => {
 *     const idMap = await insertFixture(client, expenseScenarioA);
 *
 *     // Verify insertion
 *     const result = await client.query(
 *       'SELECT * FROM accounter_schema.charges WHERE id = $1',
 *       [expenseScenarioA.charges!.charges[0].id!]
 *     );
 *     expect(result.rows).toHaveLength(1);
 *   })
 * );
 * ```
 */
export async function insertFixture(client, fixture, adminBusinessId) {
    // Validate fixture before insertion
    assertValidFixture(fixture);
    // Set RLS context if possible to allow insertion into protected tables (e.g. charges)
    const contextBusinessId = adminBusinessId || fixture.businesses?.businesses?.[0]?.id;
    if (contextBusinessId) {
        try {
            await client.query(`SELECT
          set_config('app.current_business_id', $1, true),
          set_config('app.current_user_id', $1, true),
          set_config('app.auth_type', 'jwt', true)`, [contextBusinessId]);
        }
        catch (error) {
            // Ignore error if function doesn't exist (e.g. in some mock setups) 
            // or if we can't set config for some reason.
            // The insertion might still succeed if RLS isn't fully active or checks are loose.
            console.warn('Failed to set RLS context in insertFixture:', error);
        }
    }
    const idMapping = new Map();
    // Helper to execute a section with savepoint protection
    async function executeSavepointSection(sectionName, fn) {
        // Savepoint names must be valid PostgreSQL identifiers (no spaces, start with letter)
        const savepointName = `sp_${sectionName.replace(/[^a-zA-Z0-9_]/g, '_')}`;
        try {
            await client.query(`SAVEPOINT ${savepointName}`);
            const result = await fn();
            await client.query(`RELEASE SAVEPOINT ${savepointName}`);
            return result;
        }
        catch (error) {
            await client.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
            throw new FixtureInsertionError(sectionName, error instanceof Error ? error : new Error(String(error)), { savepointName });
        }
    }
    // 1. Insert businesses (financial entities of type 'business')
    if (fixture.businesses?.businesses && fixture.businesses.businesses.length > 0) {
        await executeSavepointSection('businesses', async () => {
            for (const business of fixture.businesses.businesses) {
                // Insert financial entity first (type='business')
                // Field mapping: business.name used for display (required); hebrewName is legacy/optional
                const { id: entityId } = await ensureFinancialEntity(client, {
                    id: business.id,
                    name: business.name || business.id,
                    type: 'business',
                    ownerId: adminBusinessId,
                });
                const { id: businessId, ...options } = business;
                // Insert business details - note: vat_number column maps to governmentId field
                await ensureBusinessForEntity(client, entityId, { ...options, ownerId: adminBusinessId ?? options.ownerId });
                idMapping.set(business.id, entityId);
            }
        });
    }
    // 2. Insert tax categories (financial entities of type 'tax_category')
    if (fixture.taxCategories?.taxCategories && fixture.taxCategories.taxCategories.length > 0) {
        await executeSavepointSection('tax_categories', async () => {
            for (const taxCategory of fixture.taxCategories.taxCategories) {
                // Insert financial entity first
                const { id: entityId } = await ensureFinancialEntity(client, {
                    id: taxCategory.id,
                    name: taxCategory.name || taxCategory.id,
                    type: 'tax_category',
                    ownerId: adminBusinessId,
                });
                // Insert tax category details
                await ensureTaxCategoryForEntity(client, entityId, {
                    name: taxCategory.name,
                    hashavshevetName: taxCategory.hashavshevetName,
                    taxExcluded: taxCategory.taxExcluded,
                    ownerId: adminBusinessId ?? taxCategory.ownerId,
                });
                idMapping.set(taxCategory.id, entityId);
            }
        });
    }
    // 3. Insert financial accounts
    if (fixture.accounts?.accounts && fixture.accounts.accounts.length > 0) {
        await executeSavepointSection('financial_accounts', async () => {
            for (const account of fixture.accounts.accounts) {
                const result = await client.query(`INSERT INTO ${qualifyTable('financial_accounts')} (
            account_number, account_name, private_business, owner, type, owner_id
          )
          VALUES ($1, $2, $3, $4, $5, $6)
          RETURNING id, account_number`, [
                    account.accountNumber,
                    account.name,
                    account.privateBusiness,
                    account.ownerId,
                    account.type,
                    account.ownerId,
                ]);
                // Store both the account_number and the auto-generated UUID id
                // Transactions reference the id field, not account_number
                if (result.rows.length > 0) {
                    // Store account_number mapping for reference
                    idMapping.set(account.accountNumber, result.rows[0].account_number);
                    // Also store the UUID id for transaction references
                    if (account.accountNumber) {
                        idMapping.set(`${account.accountNumber}_id`, result.rows[0].id);
                    }
                }
            }
        });
    }
    // 3b. Insert financial account tax category mappings
    if (fixture.accountTaxCategories?.mappings && fixture.accountTaxCategories.mappings.length > 0) {
        await executeSavepointSection('account_tax_categories', async () => {
            for (const mapping of fixture.accountTaxCategories.mappings) {
                // Prefer the UUID cached from step 3 (financial_accounts insert) to avoid
                // non-deterministic SELECT when duplicate account_number rows exist in DB.
                const cachedAccountId = idMapping.get(`${mapping.accountNumber}_id`);
                let accountId;
                if (cachedAccountId) {
                    accountId = cachedAccountId;
                }
                else {
                    // Fallback: query DB, using LIMIT 1 for determinism
                    const accountResult = await client.query(`SELECT id FROM ${qualifyTable('financial_accounts')} WHERE account_number = $1 LIMIT 1`, [mapping.accountNumber]);
                    if (accountResult.rows.length === 0) {
                        throw new Error(`Financial account with account_number '${mapping.accountNumber}' not found`);
                    }
                    accountId = accountResult.rows[0].id;
                }
                await client.query(`INSERT INTO ${qualifyTable('financial_accounts_tax_categories')} (
            financial_account_id, currency, tax_category_id, owner_id
          )
          VALUES ($1, $2, $3, $4)
          ON CONFLICT (financial_account_id, currency) DO UPDATE
            SET tax_category_id = EXCLUDED.tax_category_id,
                owner_id = EXCLUDED.owner_id`, [accountId, mapping.currency, mapping.taxCategoryId, mapping.ownerId]);
            }
        });
    }
    // 4. Insert charges
    if (fixture.charges?.charges && fixture.charges.charges.length > 0) {
        await executeSavepointSection('charges', async () => {
            for (const charge of fixture.charges.charges) {
                // Set RLS context for each charge to ensure policy compliance
                if (charge.owner_id) {
                    try {
                        await client.query("SELECT set_config('app.current_business_id', $1, true)", [charge.owner_id]);
                    }
                    catch (e) {
                        // Ignore if function assumes string UUIDs but we have something else, or permission issues
                        console.warn('Failed to set RLS context for charge in fixture-loader:', e);
                    }
                }
                await client.query(`INSERT INTO ${qualifyTable('charges')} (
            id, owner_id, type, accountant_status, user_description,
            tax_category_id, optional_vat, documents_optional_flag,
            is_property, created_at, updated_at
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
          ON CONFLICT (id) DO NOTHING`, [
                    charge.id,
                    charge.owner_id,
                    charge.type ?? null,
                    charge.accountant_status ?? 'PENDING',
                    charge.user_description,
                    charge.tax_category_id ?? null,
                    charge.optional_vat ?? false,
                    charge.documents_optional_flag ?? false,
                    charge.is_property ?? false,
                ]);
                idMapping.set(charge.id, charge.id);
            }
        });
    }
    // 5. Insert transactions
    if (fixture.transactions?.transactions && fixture.transactions.transactions.length > 0) {
        await executeSavepointSection('transactions', async () => {
            for (const transaction of fixture.transactions.transactions) {
                // Set RLS context for each transaction
                if (transaction.business_id) {
                    try {
                        await client.query("SELECT set_config('app.current_business_id', $1, true)", [transaction.business_id]);
                    }
                    catch (e) {
                        console.warn('Failed to set RLS context for transaction in fixture-loader:', e);
                    }
                }
                // If account_id looks like an account_number (not a UUID), look up the actual UUID.
                // Prefer the UUID cached from step 3 (financial_accounts insert) to avoid
                // non-deterministic SELECT when duplicate account_number rows exist in DB.
                let accountId = transaction.account_id;
                if (accountId && !accountId.match(UUID_REGEX)) {
                    const cachedAccountId = idMapping.get(`${accountId}_id`);
                    if (cachedAccountId) {
                        accountId = cachedAccountId;
                    }
                    else {
                        // Fallback: query DB, using LIMIT 1 for determinism
                        const accountResult = await client.query(`SELECT id FROM ${qualifyTable('financial_accounts')} WHERE account_number = $1 LIMIT 1`, [accountId]);
                        if (accountResult.rows.length === 0) {
                            throw new Error(`Financial account with account_number '${accountId}' not found`);
                        }
                        accountId = accountResult.rows[0].id;
                    }
                }
                // Insert directly into transactions_raw_list with etherscan_id to satisfy check constraint
                // This avoids triggering any creditcard/bank transaction handlers which would auto-create charges
                // etherscan_id is used because it has no INSERT trigger and is a simple UUID reference
                const dummyEtherscanId = transaction.id ? makeUUID('raw-transaction', `etherscan-${transaction.id}`) : makeUUIDLegacy();
                const rawListResult = await client.query(`INSERT INTO ${qualifyTable('transactions_raw_list')} (etherscan_id)
           VALUES ($1)
           RETURNING id`, [dummyEtherscanId]);
                // Always use the newly created raw list ID as source_id
                const sourceId = rawListResult.rows[0].id;
                await client.query(`INSERT INTO ${qualifyTable('transactions')} (
            id, account_id, charge_id, source_id, source_description,
            currency, event_date, debit_date, amount, current_balance,
            business_id, is_fee, source_reference, source_origin, origin_key,
            currency_rate, owner_id, created_at, updated_at
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), NOW())
          ON CONFLICT (id) DO NOTHING`, [
                    transaction.id,
                    accountId,
                    transaction.charge_id,
                    sourceId,
                    transaction.source_description,
                    transaction.currency,
                    transaction.event_date,
                    transaction.debit_date,
                    transaction.amount,
                    transaction.current_balance ?? '0',
                    transaction.business_id,
                    transaction.is_fee ?? false,
                    'TEST-REF', // source_reference (NOT NULL)
                    'TEST', // source_origin (NOT NULL)
                    dummyEtherscanId, // origin_key (NOT NULL) - must match etherscan_id in raw list
                    transaction.currency_rate ?? 1,
                    transaction.owner_id,
                ]);
                idMapping.set(transaction.id, transaction.id);
            }
        });
    }
    // 6. Insert documents
    if (fixture.documents?.documents && fixture.documents.documents.length > 0) {
        await executeSavepointSection('documents', async () => {
            for (const document of fixture.documents.documents) {
                await client.query(`INSERT INTO ${qualifyTable('documents')} (
            id, image_url, file_url, type, serial_number, date,
            total_amount, currency_code, vat_amount, charge_id,
            debtor_id, creditor_id, vat_report_date_override,
            no_vat_amount, allocation_number, exchange_rate_override, file_hash, owner_id
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
          ON CONFLICT (id) DO NOTHING`, [
                    document.id,
                    document.image_url,
                    document.file_url,
                    document.type,
                    document.serial_number,
                    document.date,
                    document.total_amount,
                    document.currency_code,
                    document.vat_amount,
                    document.charge_id,
                    document.debtor_id,
                    document.creditor_id,
                    document.vat_report_date_override,
                    document.no_vat_amount,
                    document.allocation_number,
                    document.exchange_rate_override,
                    document.file_hash,
                    document.owner_id,
                ]);
                idMapping.set(document.id, document.id);
            }
        });
    }
    return idMapping;
}
//# sourceMappingURL=fixture-loader.js.map