import type { Mocked } from 'vitest';

import { createMockServer, createMockTransport, createMockTool } from '../../__tests__/helpers';
import { SessionManager } from '../SessionManager';
import type { SessionStore } from '../SessionStore';

describe('SessionManager', () => {
	let manager: SessionManager;
	let mockStore: Mocked<SessionStore>;

	beforeEach(() => {
		mockStore = {
			register: vi.fn().mockResolvedValue(undefined),
			validate: vi.fn().mockResolvedValue(true),
			unregister: vi.fn().mockResolvedValue(undefined),
			getTools: vi.fn(),
			setTools: vi.fn(),
			clearTools: vi.fn(),
		};
		manager = new SessionManager(mockStore);
	});

	describe('registerSession', () => {
		it('should register session with server and transport', async () => {
			const server = createMockServer();
			const transport = createMockTransport('session-1');

			await manager.registerSession('session-1', server, transport);

			expect(mockStore.register).toHaveBeenCalledWith('session-1');
			expect(manager.getSession('session-1')).toMatchObject({
				sessionId: 'session-1',
				server,
				transport,
			});
		});

		it('should store tools when provided', async () => {
			const tools = [createMockTool('tool-1')];
			await manager.registerSession(
				'session-1',
				createMockServer(),
				createMockTransport('session-1'),
				tools,
			);

			expect(mockStore.setTools).toHaveBeenCalledWith('session-1', tools);
		});

		it('should not call setTools when no tools provided', async () => {
			await manager.registerSession(
				'session-1',
				createMockServer(),
				createMockTransport('session-1'),
			);

			expect(mockStore.setTools).not.toHaveBeenCalled();
		});

		it('should not register if sessionId is empty', async () => {
			await manager.registerSession('', createMockServer(), createMockTransport(''));

			expect(mockStore.register).not.toHaveBeenCalled();
			expect(manager.getSession('')).toBeUndefined();
		});

		it('should overwrite existing session when registering same sessionId', async () => {
			const server1 = createMockServer();
			const transport1 = createMockTransport('session-1');
			const server2 = createMockServer();
			const transport2 = createMockTransport('session-1');

			await manager.registerSession('session-1', server1, transport1);
			await manager.registerSession('session-1', server2, transport2);

			const session = manager.getSession('session-1');
			expect(session?.server).toBe(server2);
			expect(session?.transport).toBe(transport2);
		});
	});

	describe('destroySession', () => {
		it('should remove session and delegate to store', async () => {
			await manager.registerSession(
				'session-1',
				createMockServer(),
				createMockTransport('session-1'),
			);
			await manager.destroySession('session-1');

			expect(mockStore.unregister).toHaveBeenCalledWith('session-1');
			expect(manager.getSession('session-1')).toBeUndefined();
		});

		it('should handle destroying non-existent session', async () => {
			await expect(manager.destroySession('non-existent')).resolves.not.toThrow();
			expect(mockStore.unregister).toHaveBeenCalledWith('non-existent');
		});

		it('should close the server it held to release the transport', async () => {
			const server = createMockServer();
			await manager.registerSession('session-1', server, createMockTransport('session-1'));

			await manager.destroySession('session-1');

			expect(server.close).toHaveBeenCalled();
		});

		it('should not recurse when closing the server re-enters destroySession', async () => {
			const server = createMockServer();
			// Production wires transport.onclose -> cleanupSession -> destroySession, so
			// server.close() re-enters this method for the same session.
			let reentered = false;
			server.close.mockImplementation(async () => {
				if (!reentered) {
					reentered = true;
					await manager.destroySession('session-1');
				}
			});
			await manager.registerSession('session-1', server, createMockTransport('session-1'));

			await expect(manager.destroySession('session-1')).resolves.not.toThrow();

			// Deleted before close, so the re-entrant call finds nothing and skips close.
			expect(server.close).toHaveBeenCalledTimes(1);
			expect(manager.getSession('session-1')).toBeUndefined();
		});
	});

	describe('getSession', () => {
		it('should return session info for registered session', async () => {
			const server = createMockServer();
			const transport = createMockTransport('session-1');
			await manager.registerSession('session-1', server, transport);

			const session = manager.getSession('session-1');

			expect(session).toMatchObject({
				sessionId: 'session-1',
				server,
				transport,
			});
		});

		it('should return undefined for unregistered session', () => {
			expect(manager.getSession('non-existent')).toBeUndefined();
		});
	});

	describe('activity tracking', () => {
		beforeEach(() => {
			vi.useFakeTimers();
		});

		afterEach(() => {
			vi.useRealTimers();
		});

		it('should stamp lastActivityAt on register', async () => {
			vi.setSystemTime(1_000);
			await manager.registerSession(
				'session-1',
				createMockServer(),
				createMockTransport('session-1'),
			);

			expect(manager.getSession('session-1')?.lastActivityAt).toBe(1_000);
		});

		it('should refresh lastActivityAt on touch', async () => {
			vi.setSystemTime(1_000);
			await manager.registerSession(
				'session-1',
				createMockServer(),
				createMockTransport('session-1'),
			);

			vi.setSystemTime(5_000);
			manager.touch('session-1');

			expect(manager.getSession('session-1')?.lastActivityAt).toBe(5_000);
		});

		it('should ignore touch for unknown session without creating it', () => {
			manager.touch('non-existent');
			expect(manager.getSession('non-existent')).toBeUndefined();
		});

		describe('getIdleSessions', () => {
			it('should return sessions idle for at least the ttl', async () => {
				vi.setSystemTime(0);
				await manager.registerSession('stale', createMockServer(), createMockTransport('stale'));

				expect(manager.getIdleSessions(1_000, 2_500)).toEqual(['stale']);
			});

			it('should treat exactly-ttl as idle', async () => {
				vi.setSystemTime(1_000);
				await manager.registerSession(
					'session-1',
					createMockServer(),
					createMockTransport('session-1'),
				);

				expect(manager.getIdleSessions(1_000, 2_000)).toEqual(['session-1']);
			});

			it('should exclude recently touched sessions', async () => {
				vi.setSystemTime(0);
				await manager.registerSession('active', createMockServer(), createMockTransport('active'));
				await manager.registerSession('stale', createMockServer(), createMockTransport('stale'));

				vi.setSystemTime(5_000);
				manager.touch('active');

				expect(manager.getIdleSessions(1_000, 5_500)).toEqual(['stale']);
			});

			it('should not return destroyed sessions', async () => {
				vi.setSystemTime(0);
				await manager.registerSession(
					'session-1',
					createMockServer(),
					createMockTransport('session-1'),
				);
				await manager.destroySession('session-1');

				expect(manager.getIdleSessions(1_000, 10_000)).toEqual([]);
			});
		});
	});

	describe('getTransport', () => {
		it('should return transport for registered session', async () => {
			const transport = createMockTransport('session-1');
			await manager.registerSession('session-1', createMockServer(), transport);

			expect(manager.getTransport('session-1')).toBe(transport);
		});

		it('should return undefined for unregistered session', () => {
			expect(manager.getTransport('non-existent')).toBeUndefined();
		});
	});

	describe('getServer', () => {
		it('should return server for registered session', async () => {
			const server = createMockServer();
			await manager.registerSession('session-1', server, createMockTransport('session-1'));

			expect(manager.getServer('session-1')).toBe(server);
		});

		it('should return undefined for unregistered session', () => {
			expect(manager.getServer('non-existent')).toBeUndefined();
		});
	});

	describe('isSessionValid', () => {
		it('should delegate to store.validate and return true', async () => {
			mockStore.validate.mockResolvedValue(true);
			expect(await manager.isSessionValid('session-1')).toBe(true);
			expect(mockStore.validate).toHaveBeenCalledWith('session-1');
		});

		it('should delegate to store.validate and return false', async () => {
			mockStore.validate.mockResolvedValue(false);
			expect(await manager.isSessionValid('session-1')).toBe(false);
		});
	});

	describe('tools management', () => {
		it('should delegate getTools to store', () => {
			const tools = [createMockTool('tool-1')];
			mockStore.getTools.mockReturnValue(tools);

			expect(manager.getTools('session-1')).toBe(tools);
			expect(mockStore.getTools).toHaveBeenCalledWith('session-1');
		});

		it('should delegate setTools to store', () => {
			const tools = [createMockTool('tool-1')];
			manager.setTools('session-1', tools);

			expect(mockStore.setTools).toHaveBeenCalledWith('session-1', tools);
		});
	});

	describe('store management', () => {
		it('should allow swapping session store', () => {
			const newStore = { ...mockStore } as Mocked<SessionStore>;
			manager.setStore(newStore);
			expect(manager.getStore()).toBe(newStore);
		});

		it('should return current store', () => {
			expect(manager.getStore()).toBe(mockStore);
		});
	});
});
