import { __addDisposableResource, __disposeResources } from "tslib";
import { KeetaNet } from '../../client/index.js';
import { createNodeAndClient, setResolverInfo } from '../../lib/utils/tests/node.js';
import { KeetaNetStorageAnchorHTTPServer } from './server.js';
import { Errors } from './common.js';
import { Buffer } from '../../lib/utils/buffer.js';
import KeetaAnchorResolver from '../../lib/resolver.js';
import KeetaStorageAnchorClient from './client.js';
/**
 * Test path policy implementing the /user/<pubkey>/<path> pattern.
 * Owner-based access control: only the owner can access their namespace.
 */
export class TestPathPolicy {
    // Matches /user/<owner> or /user/<owner>/ or /user/<owner>/<path>
    #pattern = /^\/user\/([^/]+)(\/(.*))?$/;
    parse(path) {
        const match = path.match(this.#pattern);
        if (!match?.[1]) {
            return (null);
        }
        return ({ path, owner: match[1], relativePath: match[3] ?? '' });
    }
    validate(path) {
        const parsed = this.parse(path);
        if (!parsed) {
            throw (new Errors.InvalidPath('Path must match /user/<pubkey>/<path>'));
        }
        // Reject empty segments in original path
        if (path.includes('//')) {
            throw (new Errors.InvalidPath('Path contains empty segments'));
        }
        // Reject path traversal attempts
        const segments = parsed.relativePath.split('/');
        for (const seg of segments) {
            if (seg === '..' || seg === '.') {
                throw (new Errors.InvalidPath('Path contains relative segments'));
            }
        }
        return (parsed);
    }
    isValid(path) {
        return (this.parse(path) !== null);
    }
    checkAccess(account, parsed, _ignoreOperation) {
        // Owner-based access: account must match the path owner
        return (parsed.owner === account.publicKeyString.get());
    }
    getAuthorizedSigner(parsed) {
        return (KeetaNet.lib.Account.fromPublicKeyString(parsed.owner).assertAccount());
    }
    getOwner(path) {
        return (this.validate(path).owner);
    }
    validateContext(parsed, context) {
        if (context.operation === 'put' || context.operation === 'updateMetadata') {
            if (parsed.relativePath.startsWith('public/') && context.metadata.visibility !== 'public') {
                throw (new Errors.InvalidMetadata('Objects under /public/ must have public visibility'));
            }
        }
    }
    /**
     * Helper to construct a path for a given owner and relative path.
     */
    makePath(owner, relativePath) {
        return (`/user/${owner}/${relativePath}`);
    }
    /**
     * Helper to get the namespace prefix for an owner.
     */
    getNamespacePrefix(owner) {
        return (`/user/${owner}/`);
    }
}
/**
 * Shared instance of TestPathPolicy for use in tests.
 */
export const testPathPolicy = new TestPathPolicy();
/**
 * Create test metadata with sensible defaults.
 */
export function testMetadata(owner, overrides) {
    return ({
        owner,
        tags: [],
        visibility: 'private',
        ...overrides
    });
}
/**
 * Create test object metadata with sensible defaults.
 */
export function testObjectMetadata(path, owner, overrides) {
    return ({
        path,
        owner,
        tags: [],
        visibility: 'private',
        size: 0,
        createdAt: new Date().toISOString(),
        ...overrides
    });
}
// #endregion
// #region Shared Storage Operations
function putToStorage(storage, path, data, metadata) {
    const now = new Date().toISOString();
    const existing = storage.get(path);
    const objectMetadata = {
        path,
        owner: metadata.owner,
        tags: metadata.tags,
        visibility: metadata.visibility,
        size: data.length,
        createdAt: existing?.metadata.createdAt ?? now,
        ...(existing ? { updatedAt: now } : {})
    };
    storage.set(path, { data, metadata: objectMetadata });
    return (objectMetadata);
}
function getFromStorage(storage, path) {
    const entry = storage.get(path);
    if (!entry) {
        return (null);
    }
    return ({
        data: Buffer.from(entry.data),
        metadata: { ...entry.metadata }
    });
}
function searchStorage(storage, criteria, pagination) {
    const results = [];
    const limit = pagination.limit ?? 100;
    const startAfter = pagination.cursor;
    let foundCursor = !startAfter;
    for (const [path, entry] of storage.entries()) {
        if (!foundCursor) {
            if (path === startAfter) {
                foundCursor = true;
            }
            continue;
        }
        const metadata = entry.metadata;
        if (criteria.pathPrefix) {
            const prefix = criteria.pathPrefix.endsWith('/')
                ? criteria.pathPrefix
                : criteria.pathPrefix + '/';
            if (!path.startsWith(prefix) && path !== criteria.pathPrefix) {
                continue;
            }
            if (!criteria.recursive) {
                const remainder = path.slice(prefix.length);
                if (remainder.includes('/')) {
                    continue;
                }
            }
        }
        if (criteria.owner && metadata.owner !== criteria.owner) {
            continue;
        }
        if (criteria.tags && criteria.tags.length > 0) {
            const hasMatchingTag = criteria.tags.some(function (tag) {
                return (metadata.tags.includes(tag));
            });
            if (!hasMatchingTag) {
                continue;
            }
        }
        if (criteria.name) {
            const filename = path.split('/').pop();
            if (!filename?.includes(criteria.name)) {
                continue;
            }
        }
        if (criteria.visibility && metadata.visibility !== criteria.visibility) {
            continue;
        }
        results.push(metadata);
        if (results.length >= limit) {
            return ({ results, nextCursor: path });
        }
    }
    return ({ results });
}
function computeQuotaStatus(storage, owner, quotaConfig) {
    let objectCount = 0;
    let totalSize = 0;
    for (const entry of storage.values()) {
        if (entry.metadata.owner === owner) {
            objectCount++;
            totalSize += entry.metadata.size;
        }
    }
    return ({
        objectCount,
        totalSize,
        remainingObjects: Math.max(0, quotaConfig.maxObjectsPerUser - objectCount),
        remainingSize: Math.max(0, quotaConfig.maxStoragePerUser - totalSize)
    });
}
// #endregion
// #region Memory Storage Backend
/**
 * In-memory storage backend with full capabilities: CRUD, search, and quota management.
 * Intended for testing and development purposes.
 */
export class MemoryStorageBackend {
    storage = new Map();
    reservations = new Map();
    #reservationsByPath = new Map(); // "owner:path" -> reservationId
    reservationCounter = 0;
    #quotaLimitsPerUser = new Map();
    quotaConfig = {
        maxObjectsPerUser: 1000,
        maxStoragePerUser: 100 * 1024 * 1024 // 100MB
    };
    /**
     * Prune expired reservations to keep quota accounting accurate.
     */
    #pruneExpiredReservations() {
        const now = Date.now();
        for (const [id, reservation] of this.reservations) {
            if (new Date(reservation.expiresAt).getTime() <= now) {
                this.reservations.delete(id);
                this.#reservationsByPath.delete(`${reservation.owner}:${reservation.path}`);
            }
        }
    }
    async put(path, data, metadata) {
        return (putToStorage(this.storage, path, data, metadata));
    }
    async get(path) {
        return (getFromStorage(this.storage, path));
    }
    async delete(path) {
        return (this.storage.delete(path));
    }
    async updateMetadata(path, metadata) {
        const entry = this.storage.get(path);
        if (!entry) {
            return (null);
        }
        const now = new Date().toISOString();
        const updated = {
            ...entry.metadata,
            tags: metadata.tags,
            visibility: metadata.visibility,
            updatedAt: now
        };
        this.storage.set(path, { data: entry.data, metadata: updated });
        return (updated);
    }
    async search(criteria, pagination) {
        return (searchStorage(this.storage, criteria, pagination));
    }
    async getQuotaStatus(owner) {
        // Prune expired reservations first
        this.#pruneExpiredReservations();
        // Get base quota from actual storage
        const baseQuota = computeQuotaStatus(this.storage, owner, this.quotaConfig);
        // Add pending reservations for this owner
        let reservedObjects = 0;
        let reservedSize = 0;
        for (const reservation of this.reservations.values()) {
            if (reservation.owner === owner) {
                // Only count as new object if path doesn't exist
                if (!this.storage.has(reservation.path)) {
                    reservedObjects++;
                }
                reservedSize += reservation.size;
            }
        }
        const objectCount = baseQuota.objectCount + reservedObjects;
        const totalSize = baseQuota.totalSize + reservedSize;
        const remainingObjects = Math.max(0, (baseQuota.remainingObjects ?? 0) - reservedObjects);
        const remainingSize = Math.max(0, (baseQuota.remainingSize ?? 0) - reservedSize);
        return ({
            objectCount,
            totalSize,
            remainingObjects,
            remainingSize
        });
    }
    async getQuotaLimits(owner) {
        return (this.#quotaLimitsPerUser.get(owner) ?? null);
    }
    setQuotaLimits(owner, limits) {
        this.#quotaLimitsPerUser.set(owner, limits);
    }
    async reserveUpload(owner, path, size, options) {
        // Validate size parameter
        if (size < 0) {
            throw (new Errors.InvariantViolation('Reservation size cannot be negative'));
        }
        // Prune expired reservations first
        this.#pruneExpiredReservations();
        // Default TTL: 5 minutes
        const DEFAULT_RESERVATION_TTL_MS = 5 * 60 * 1000;
        const ttl = options?.ttlMs ?? DEFAULT_RESERVATION_TTL_MS;
        // Use provided quota limits or fall back to backend defaults
        const limits = options?.quotaLimits ?? this.quotaConfig;
        // Check if this would exceed quota
        const quotaStatus = await this.getQuotaStatus(owner);
        const isNewObject = !this.storage.has(path);
        const existingSize = this.storage.get(path)?.data.length ?? 0;
        const sizeDelta = size - existingSize;
        // Calculate remaining based on provided limits
        const remainingObjects = limits.maxObjectsPerUser - quotaStatus.objectCount;
        const remainingSize = limits.maxStoragePerUser - quotaStatus.totalSize;
        if (isNewObject && remainingObjects <= 0) {
            throw (new Errors.QuotaExceeded({
                quotaType: 'maxObjectsPerUser',
                limit: limits.maxObjectsPerUser,
                current: quotaStatus.objectCount
            }));
        }
        if (sizeDelta > 0 && remainingSize < sizeDelta) {
            throw (new Errors.QuotaExceeded({
                quotaType: 'maxStoragePerUser',
                limit: limits.maxStoragePerUser,
                current: quotaStatus.totalSize + sizeDelta
            }));
        }
        // Check for existing reservation for this (owner, path)
        // If a reservation already exists, we calculate the additional quota needed
        // by comparing the new sizeDelta with the existing reservation's reserved size.
        const pathKey = `${owner}:${path}`;
        const existingId = this.#reservationsByPath.get(pathKey);
        if (existingId) {
            const existing = this.reservations.get(existingId);
            if (existing) {
                const clampedSizeDelta = Math.max(0, sizeDelta);
                const additionalSize = clampedSizeDelta - existing.size;
                // Re-check quota if size is increasing beyond current reservation
                if (additionalSize > 0 && remainingSize < additionalSize) {
                    throw (new Errors.QuotaExceeded({
                        quotaType: 'maxStoragePerUser',
                        limit: limits.maxStoragePerUser,
                        current: quotaStatus.totalSize + additionalSize
                    }));
                }
                // Update to max size, extend expiry
                existing.size = Math.max(existing.size, clampedSizeDelta);
                existing.expiresAt = new Date(Date.now() + ttl).toISOString();
                return (existing);
            }
        }
        const now = new Date();
        const reservation = {
            id: `res_${++this.reservationCounter}`,
            owner,
            path,
            size: Math.max(0, sizeDelta),
            createdAt: now.toISOString(),
            expiresAt: new Date(now.getTime() + ttl).toISOString()
        };
        this.reservations.set(reservation.id, reservation);
        this.#reservationsByPath.set(pathKey, reservation.id);
        return (reservation);
    }
    async commitUpload(reservationId) {
        // Simply remove the reservation - the actual storage was already updated via put()
        const reservation = this.reservations.get(reservationId);
        if (reservation) {
            this.#reservationsByPath.delete(`${reservation.owner}:${reservation.path}`);
        }
        this.reservations.delete(reservationId);
    }
    async releaseUpload(reservationId) {
        // Remove the reservation, freeing the reserved quota
        const reservation = this.reservations.get(reservationId);
        if (reservation) {
            this.#reservationsByPath.delete(`${reservation.owner}:${reservation.path}`);
        }
        this.reservations.delete(reservationId);
    }
    clear() {
        this.storage.clear();
        this.reservations.clear();
        this.#reservationsByPath.clear();
    }
    get size() {
        return (this.storage.size);
    }
}
export function randomSeed() {
    return (KeetaNet.lib.Account.generateRandomSeed());
}
/**
 * Shared test harness that stands up a storage server, resolves a provider,
 * and passes the provider context to the test function.
 */
export async function withStorageProvider(seed, testFunction) {
    const env_1 = { stack: [], error: void 0, hasError: false };
    try {
        const account = KeetaNet.lib.Account.fromSeed(seed, 0);
        const anchorAccount = KeetaNet.lib.Account.fromSeed(seed, 50);
        const nodeAndClient = __addDisposableResource(env_1, await createNodeAndClient(account), true);
        const userClient = nodeAndClient.userClient;
        nodeAndClient.fees.disable();
        const backend = new MemoryStorageBackend();
        const server = __addDisposableResource(env_1, new KeetaNetStorageAnchorHTTPServer({
            backend,
            anchorAccount,
            pathPolicies: [testPathPolicy]
        }), true);
        await server.start();
        const rootAccount = KeetaNet.lib.Account.fromSeed(seed, 100);
        const serviceMetadata = await server.serviceMetadata();
        await setResolverInfo(rootAccount, userClient, {
            version: 1,
            currencyMap: {},
            services: {
                storage: {
                    'test-provider': serviceMetadata
                }
            }
        });
        const resolver = new KeetaAnchorResolver({
            root: rootAccount,
            client: userClient,
            trustedCAs: []
        });
        const storageClient = new KeetaStorageAnchorClient(userClient, { resolver });
        const maybeProvider = await storageClient.getProviderByID('test-provider');
        if (!maybeProvider) {
            throw (new Error('Provider not found'));
        }
        await testFunction({ provider: maybeProvider, account, storageClient });
    }
    catch (e_1) {
        env_1.error = e_1;
        env_1.hasError = true;
    }
    finally {
        const result_1 = __disposeResources(env_1);
        if (result_1)
            await result_1;
    }
}
//# sourceMappingURL=test-utils.js.map