import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExpressionEvaluator } from '../expression-evaluator';
import type { RuntimeBridge, ObservabilityProvider } from '../../types';
import { createMockBridge, createMockObservability } from '../../__tests__/helpers';

describe('ExpressionEvaluator cache', () => {
	let bridge: RuntimeBridge;
	let observability: ObservabilityProvider;

	beforeEach(() => {
		vi.clearAllMocks();
		bridge = createMockBridge();
		observability = createMockObservability();
	});

	it('should emit cache miss on first evaluation', async () => {
		const evaluator = new ExpressionEvaluator({
			createBridge: () => bridge,
			observability,
			maxCodeCacheSize: 1024,
		});
		await evaluator.initialize();
		const caller = {};
		await evaluator.acquire(caller);
		evaluator.evaluate('={{ $json.email }}', {}, caller);
		expect(observability.metrics.counter).toHaveBeenCalledWith('expression.code_cache.miss', 1);
	});

	it('should emit cache hit on repeated evaluation', async () => {
		const evaluator = new ExpressionEvaluator({
			createBridge: () => bridge,
			observability,
			maxCodeCacheSize: 1024,
		});
		await evaluator.initialize();
		const caller = {};
		await evaluator.acquire(caller);
		evaluator.evaluate('={{ $json.email }}', {}, caller);
		evaluator.evaluate('={{ $json.email }}', {}, caller);
		expect(observability.metrics.counter).toHaveBeenCalledWith('expression.code_cache.hit', 1);
	});

	it('should emit eviction when cache is full', async () => {
		const evaluator = new ExpressionEvaluator({
			createBridge: () => bridge,
			observability,
			maxCodeCacheSize: 2,
		});
		await evaluator.initialize();
		const caller = {};
		await evaluator.acquire(caller);
		evaluator.evaluate('={{ $json.a }}', {}, caller);
		evaluator.evaluate('={{ $json.b }}', {}, caller);
		evaluator.evaluate('={{ $json.c }}', {}, caller); // evicts first
		expect(observability.metrics.counter).toHaveBeenCalledWith('expression.code_cache.eviction', 1);
	});

	it('should work without observability', async () => {
		const evaluator = new ExpressionEvaluator({
			createBridge: () => bridge,
			maxCodeCacheSize: 1024,
		});
		await evaluator.initialize();
		const caller = {};
		await evaluator.acquire(caller);
		expect(() => {
			evaluator.evaluate('={{ $json.email }}', {}, caller);
			evaluator.evaluate('={{ $json.email }}', {}, caller);
		}).not.toThrow();
	});

	it('should emit cache size gauge on cache miss', async () => {
		const evaluator = new ExpressionEvaluator({
			createBridge: () => bridge,
			observability,
			maxCodeCacheSize: 1024,
		});
		await evaluator.initialize();
		const caller = {};
		await evaluator.acquire(caller);
		evaluator.evaluate('={{ $json.email }}', {}, caller);
		expect(observability.metrics.gauge).toHaveBeenCalledWith('expression.code_cache.size', 1);
	});

	it('should emit cache size gauge of 0 on dispose', async () => {
		const evaluator = new ExpressionEvaluator({
			createBridge: () => bridge,
			observability,
			maxCodeCacheSize: 1024,
		});
		await evaluator.initialize();
		const caller = {};
		await evaluator.acquire(caller);
		evaluator.evaluate('={{ $json.email }}', {}, caller);
		vi.clearAllMocks();
		await evaluator.dispose();
		expect(observability.metrics.gauge).toHaveBeenCalledWith('expression.code_cache.size', 0);
	});

	it('should evict least recently used and report miss on re-access', async () => {
		const evaluator = new ExpressionEvaluator({
			createBridge: () => bridge,
			observability,
			maxCodeCacheSize: 2,
		});
		await evaluator.initialize();
		const caller = {};
		await evaluator.acquire(caller);
		evaluator.evaluate('={{ $json.a }}', {}, caller);
		evaluator.evaluate('={{ $json.b }}', {}, caller);
		evaluator.evaluate('={{ $json.c }}', {}, caller);
		expect(observability.metrics.counter).toHaveBeenCalledWith('expression.code_cache.eviction', 1);
		vi.clearAllMocks();
		evaluator.evaluate('={{ $json.a }}', {}, caller);
		expect(observability.metrics.counter).toHaveBeenCalledWith('expression.code_cache.miss', 1);
	});
});
