/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * Testes unitários para trace-context.ts
 */

import { beforeEach as beforeEach, describe, expect, it } from '@jest/globals'
import {
	addSpanEvent,
	createPrecisionTimer,
	createSpan,
	createTraceContext,
	endSpan,
	exportContext,
	extractTraceHeaders,
	generateCorrelationId,
	generateSpanId,
	generateTraceId,
	getAllBaggage,
	getBaggage,
	getCurrentContext,
	getOrCreateContext,
	importContext,
	injectTraceHeaders,
	removeBaggage,
	runWithContext,
	runWithNewContext,
	setBaggage,
	setSpanAttributes,
	setSpanError,
	type Span,
	startSpan,
	TRACE_HEADERS,
	type TraceContext,
	withSpan,
	withSpanSync
} from '../../Utils/trace-context.js'

describe('ID generation', () => {
	describe('generateTraceId', () => {
		it('should generate 32 character hex string', () => {
			const id = generateTraceId()
			expect(id).toHaveLength(32)
			expect(id).toMatch(/^[0-9a-f]+$/)
		})

		it('should generate unique IDs', () => {
			const ids = new Set()
			for (let i = 0; i < 100; i++) {
				ids.add(generateTraceId())
			}

			expect(ids.size).toBe(100)
		})
	})

	describe('generateSpanId', () => {
		it('should generate 16 character hex string', () => {
			const id = generateSpanId()
			expect(id).toHaveLength(16)
			expect(id).toMatch(/^[0-9a-f]+$/)
		})
	})

	describe('generateCorrelationId', () => {
		it('should generate readable correlation ID', () => {
			const id = generateCorrelationId()
			expect(id).toMatch(/^[a-z0-9]+-[a-z0-9]+$/)
		})
	})
})

describe('TraceContext', () => {
	describe('createTraceContext', () => {
		it('should create context with generated IDs', () => {
			const context = createTraceContext()

			expect(context.traceIds.traceId).toHaveLength(32)
			expect(context.traceIds.spanId).toHaveLength(16)
			expect(context.traceIds.correlationId).toBeTruthy()
			expect(context.baggage).toEqual({})
			expect(context.spanStack).toEqual([])
		})

		it('should use provided options', () => {
			const context = createTraceContext({
				traceId: 'custom-trace-id',
				correlationId: 'custom-corr-id',
				parentSpanId: 'parent-span',
				baggage: { key: 'value' },
				metadata: { env: 'test' }
			})

			expect(context.traceIds.traceId).toBe('custom-trace-id')
			expect(context.traceIds.correlationId).toBe('custom-corr-id')
			expect(context.traceIds.parentSpanId).toBe('parent-span')
			expect(context.baggage).toEqual({ key: 'value' })
			expect(context.metadata).toEqual({ env: 'test' })
		})
	})

	describe('runWithContext', () => {
		it('should make context available within scope', () => {
			const context = createTraceContext()
			let captured: TraceContext | undefined

			runWithContext(context, () => {
				captured = getCurrentContext()
			})

			expect(captured).toBe(context)
		})

		it('should not leak context outside scope', () => {
			const context = createTraceContext()

			runWithContext(context, () => {
				// Context available here
			})

			// Context should be undefined outside
			expect(getCurrentContext()).toBeUndefined()
		})

		it('should return value from function', () => {
			const context = createTraceContext()

			const result = runWithContext(context, () => 'result')

			expect(result).toBe('result')
		})
	})

	describe('runWithNewContext', () => {
		it('should create and run with new context', () => {
			let captured: TraceContext | undefined

			runWithNewContext({ baggage: { test: 'value' } }, () => {
				captured = getCurrentContext()
			})

			expect(captured).toBeDefined()
			expect(captured!.baggage.test).toBe('value')
		})
	})

	describe('getOrCreateContext', () => {
		it('should return existing context if available', () => {
			const context = createTraceContext()

			runWithContext(context, () => {
				const retrieved = getOrCreateContext()
				expect(retrieved).toBe(context)
			})
		})

		it('should create new context if none exists', () => {
			const context = getOrCreateContext()
			expect(context).toBeDefined()
			expect(context.traceIds.traceId).toHaveLength(32)
		})
	})
})

describe('Spans', () => {
	describe('createSpan', () => {
		it('should create span with provided name', () => {
			const span = createSpan({ name: 'test-span' })

			expect(span.name).toBe('test-span')
			expect(span.traceIds.spanId).toHaveLength(16)
			expect(span.startTime).toBeGreaterThan(0)
			expect(span.status).toBe('unset')
			expect(span.ended).toBe(false)
		})

		it('should include provided attributes', () => {
			const span = createSpan({
				name: 'test',
				attributes: { key: 'value', count: 42 }
			})

			expect(span.attributes).toEqual({ key: 'value', count: 42 })
		})
	})

	describe('startSpan', () => {
		it('should start span and set as current in context', () => {
			const context = createTraceContext()

			runWithContext(context, () => {
				const span = startSpan({ name: 'operation' })

				expect(getCurrentContext()!.currentSpan).toBe(span)
			})
		})

		it('should support nested spans', () => {
			const context = createTraceContext()

			runWithContext(context, () => {
				const parent = startSpan({ name: 'parent' })
				const child = startSpan({ name: 'child' })

				expect(child.traceIds.parentSpanId).toBe(parent.traceIds.spanId)
				expect(getCurrentContext()!.spanStack).toContain(parent)
			})
		})
	})

	describe('endSpan', () => {
		it('should mark span as ended', () => {
			const span = createSpan({ name: 'test' })
			endSpan(span)

			expect(span.ended).toBe(true)
			expect(span.endTime).toBeDefined()
			expect(span.duration).toBeDefined()
			expect(span.status).toBe('ok')
		})

		it('should set custom status', () => {
			const span = createSpan({ name: 'test' })
			endSpan(span, 'error')

			expect(span.status).toBe('error')
		})

		it('should not modify already ended span', () => {
			const span = createSpan({ name: 'test' })
			endSpan(span, 'ok')

			const endTime = span.endTime

			endSpan(span, 'error')

			expect(span.endTime).toBe(endTime)
			expect(span.status).toBe('ok')
		})

		it('should pop span from stack', () => {
			const context = createTraceContext()

			runWithContext(context, () => {
				const parent = startSpan({ name: 'parent' })
				const child = startSpan({ name: 'child' })

				endSpan(child)

				expect(getCurrentContext()!.currentSpan).toBe(parent)
			})
		})
	})

	describe('addSpanEvent', () => {
		it('should add event to span', () => {
			const span = createSpan({ name: 'test' })
			addSpanEvent(span, 'event-occurred', { detail: 'value' })

			expect(span.events).toHaveLength(1)
			expect(span.events[0]!.name).toBe('event-occurred')
			expect(span.events[0]!.attributes).toEqual({ detail: 'value' })
		})

		it('should not add event to ended span', () => {
			const span = createSpan({ name: 'test' })
			endSpan(span)
			addSpanEvent(span, 'late-event')

			expect(span.events).toHaveLength(0)
		})
	})

	describe('setSpanAttributes', () => {
		it('should set attributes on span', () => {
			const span = createSpan({ name: 'test' })
			setSpanAttributes(span, { key1: 'value1', key2: 'value2' })

			expect(span.attributes.key1).toBe('value1')
			expect(span.attributes.key2).toBe('value2')
		})
	})

	describe('setSpanError', () => {
		it('should mark span as error with details', () => {
			const span = createSpan({ name: 'test' })
			const error = new Error('Something went wrong')
			setSpanError(span, error)

			expect(span.status).toBe('error')
			expect(span.attributes.error).toBe(true)
			expect(span.attributes.errorMessage).toBe('Something went wrong')
			expect(span.events.some(e => e.name === 'exception')).toBe(true)
		})
	})
})

describe('withSpan helpers', () => {
	describe('withSpan', () => {
		it('should execute async operation within span', async () => {
			const result = await withSpan('async-op', async span => {
				expect(span.name).toBe('async-op')
				return 'async-result'
			})

			expect(result).toBe('async-result')
		})

		it('should handle errors and mark span as error', async () => {
			await expect(
				withSpan('failing-op', async () => {
					throw new Error('Failure')
				})
			).rejects.toThrow('Failure')
		})
	})

	describe('withSpanSync', () => {
		it('should execute sync operation within span', () => {
			const result = withSpanSync('sync-op', span => {
				expect(span.name).toBe('sync-op')
				return 'sync-result'
			})

			expect(result).toBe('sync-result')
		})
	})
})

describe('Baggage', () => {
	it('should set and get baggage', () => {
		const context = createTraceContext()

		runWithContext(context, () => {
			setBaggage('userId', 'user-123')
			setBaggage('requestType', 'api')

			expect(getBaggage('userId')).toBe('user-123')
			expect(getBaggage('requestType')).toBe('api')
		})
	})

	it('should get all baggage', () => {
		const context = createTraceContext({ baggage: { existing: 'value' } })

		runWithContext(context, () => {
			setBaggage('new', 'item')

			const all = getAllBaggage()

			expect(all).toEqual({ existing: 'value', new: 'item' })
		})
	})

	it('should remove baggage', () => {
		const context = createTraceContext({ baggage: { toRemove: 'value' } })

		runWithContext(context, () => {
			removeBaggage('toRemove')

			expect(getBaggage('toRemove')).toBeUndefined()
		})
	})
})

describe('Header injection/extraction', () => {
	describe('injectTraceHeaders', () => {
		it('should inject trace headers', () => {
			const context = createTraceContext({
				baggage: { user: 'test' }
			})

			const headers = runWithContext(context, () => {
				return injectTraceHeaders({})
			})

			expect(headers[TRACE_HEADERS.TRACE_ID]).toBe(context.traceIds.traceId)
			expect(headers[TRACE_HEADERS.SPAN_ID]).toBe(context.traceIds.spanId)
			expect(headers[TRACE_HEADERS.CORRELATION_ID]).toBe(context.traceIds.correlationId)
			expect(headers[TRACE_HEADERS.BAGGAGE]).toContain('user=test')
		})

		it('should preserve existing headers', () => {
			const context = createTraceContext()

			const headers = runWithContext(context, () => {
				return injectTraceHeaders({ 'Content-Type': 'application/json' })
			})

			expect(headers['Content-Type']).toBe('application/json')
		})
	})

	describe('extractTraceHeaders', () => {
		it('should extract trace context from headers', () => {
			const headers = {
				[TRACE_HEADERS.TRACE_ID]: 'trace-123',
				[TRACE_HEADERS.PARENT_SPAN_ID]: 'parent-456',
				[TRACE_HEADERS.CORRELATION_ID]: 'corr-789',
				[TRACE_HEADERS.BAGGAGE]: 'key1=value1,key2=value2'
			}

			const options = extractTraceHeaders(headers)

			expect(options.traceId).toBe('trace-123')
			expect(options.parentSpanId).toBe('parent-456')
			expect(options.correlationId).toBe('corr-789')
			expect(options.baggage).toEqual({ key1: 'value1', key2: 'value2' })
		})
	})
})

describe('Context serialization', () => {
	describe('exportContext', () => {
		it('should serialize context to JSON string', () => {
			const context = createTraceContext({
				baggage: { key: 'value' },
				metadata: { env: 'test' }
			})

			const exported = exportContext(context)
			const parsed = JSON.parse(exported)

			expect(parsed.traceIds).toEqual(context.traceIds)
			expect(parsed.baggage).toEqual(context.baggage)
			expect(parsed.metadata).toEqual(context.metadata)
		})
	})

	describe('importContext', () => {
		it('should deserialize context from JSON string', () => {
			const serialized = JSON.stringify({
				traceIds: {
					traceId: 'trace-123',
					spanId: 'span-456',
					correlationId: 'corr-789'
				},
				baggage: { imported: 'value' },
				metadata: { source: 'external' }
			})

			const options = importContext(serialized)

			expect(options.traceId).toBe('trace-123')
			expect(options.parentSpanId).toBe('span-456')
			expect(options.correlationId).toBe('corr-789')
			expect(options.baggage).toEqual({ imported: 'value' })
		})

		it('should handle invalid JSON gracefully', () => {
			const options = importContext('invalid json')
			expect(options).toEqual({})
		})
	})
})

describe('PrecisionTimer', () => {
	it('should measure elapsed time', async () => {
		const timer = createPrecisionTimer()

		await new Promise(resolve => setTimeout(resolve, 20))

		const elapsed = timer.elapsed()
		expect(elapsed).toBeGreaterThan(10)
	})

	it('should format elapsed time', async () => {
		const timer = createPrecisionTimer()

		await new Promise(resolve => setTimeout(resolve, 5))

		const formatted = timer.elapsedFormatted()
		expect(formatted).toMatch(/\d+(\.\d+)?(µs|ms|s)/)
	})

	it('should stop timer', async () => {
		const timer = createPrecisionTimer()

		await new Promise(resolve => setTimeout(resolve, 10))

		const stopped = timer.stop()

		await new Promise(resolve => setTimeout(resolve, 20))

		const afterStop = timer.elapsed()

		expect(afterStop).toBe(stopped)
	})
})
