import * as assert from 'assert';
import type { OverviewFilters } from '../../../home/protocol.js';
import type { RepositoryChange, RepositoryChangeEventData } from '../../../rpc/services/types.js';
import { createAIState } from '../../shared/contexts/ai.js';
import { createIntegrationsState } from '../../shared/contexts/integrations.js';
import { createLaunchpadState } from '../../shared/contexts/launchpad.js';
import { createOnboardingState } from '../../shared/contexts/onboarding.js';
import { InMemoryStorage } from '../../shared/host/storage.js';
import type { SubscriptionActions } from '../events.js';
import { setupSubscriptions } from '../events.js';
import { createHomeState } from '../state.js';

suite('setupSubscriptions Test Suite', () => {
	test('should refresh only the inactive overview when the overview filter changes', async () => {
		const state = {
			home: createHomeState(new InMemoryStorage()),
			integrations: createIntegrationsState(),
			ai: createAIState(),
			onboarding: createOnboardingState(),
			launchpad: createLaunchpadState(),
			commands: { service: undefined },
		};

		let onOverviewFilterChanged: ((data: { filter: OverviewFilters }) => void) | undefined;

		const services = {
			home: {
				onWalkthroughProgressChanged: () => () => {},
				onPreviewChanged: () => () => {},
				onAiAllAccessBannerChanged: () => () => {},
				onOverviewRepositoryChanged: () => () => {},
				onOverviewFilterChanged: (callback: (data: { filter: OverviewFilters }) => void) => {
					onOverviewFilterChanged = callback;
					return () => {};
				},
				onFocusAccount: () => () => {},
				onAgentSessionsChanged: () => () => {},
			},
			launchpad: {
				onLaunchpadChanged: () => () => {},
			},
			config: {},
			subscription: {
				onSubscriptionChanged: () => () => {},
			},
			integrations: {
				onIntegrationsChanged: () => () => {},
			},
			repositories: {
				onDiscoveryCompleted: () => () => {},
				onRepositoriesChanged: () => () => {},
				onRepositoryChanged: () => () => {},
				getRepositoriesState: () =>
					Promise.resolve({ count: 0, openCount: 0, hasUnsafe: false, trusted: true }),
			},
			onboarding: {
				onDidChange: () => () => {},
			},
			ai: {
				onModelChanged: () => () => {},
				onStateChanged: () => () => {},
			},
		} as unknown as Parameters<typeof setupSubscriptions>[1];

		let refreshOverviewCalls = 0;
		let refreshInactiveOverviewCalls = 0;
		let syncedFilter: OverviewFilters | undefined;

		const actions = {
			refreshOverview: () => {
				refreshOverviewCalls++;
			},
			refreshInactiveOverview: () => {
				refreshInactiveOverviewCalls++;
			},
			replaceOverview: () => {},
			updateOverviewFilter: (filter: OverviewFilters) => {
				syncedFilter = filter;
				state.home.overviewFilter.set(filter);
			},
			onFocusAccount: () => {},
			onSubscriptionChanged: () => {},
			refreshLaunchpad: () => {},
			refreshAgentOverview: () => {},
			refreshActiveOverview: () => {},
		} satisfies SubscriptionActions;

		const unsubscribe = await setupSubscriptions(state, services, actions);
		const filter: OverviewFilters = {
			recent: { threshold: 'OneMonth' },
			stale: { threshold: 'OneYear', show: true, limit: 5 },
		};

		assert.ok(onOverviewFilterChanged, 'overview filter callback should be registered');
		onOverviewFilterChanged?.({ filter: filter });

		assert.deepStrictEqual(syncedFilter, filter);
		assert.deepStrictEqual(state.home.overviewFilter.get(), filter);
		assert.strictEqual(refreshInactiveOverviewCalls, 1);
		assert.strictEqual(refreshOverviewCalls, 0);

		unsubscribe();
	});

	test('should replace the overview immediately when the selected repository changes', async () => {
		const state = {
			home: createHomeState(new InMemoryStorage()),
			integrations: createIntegrationsState(),
			ai: createAIState(),
			onboarding: createOnboardingState(),
			launchpad: createLaunchpadState(),
			commands: { service: undefined },
		};

		let onOverviewRepositoryChanged: ((data: { repoPath: string | undefined }) => void) | undefined;

		const services = {
			home: {
				onWalkthroughProgressChanged: () => () => {},
				onPreviewChanged: () => () => {},
				onAiAllAccessBannerChanged: () => () => {},
				onOverviewRepositoryChanged: (callback: (data: { repoPath: string | undefined }) => void) => {
					onOverviewRepositoryChanged = callback;
					return () => {};
				},
				onOverviewFilterChanged: () => () => {},
				onFocusAccount: () => () => {},
				onAgentSessionsChanged: () => () => {},
			},
			launchpad: {
				onLaunchpadChanged: () => () => {},
			},
			config: {},
			subscription: {
				onSubscriptionChanged: () => () => {},
			},
			integrations: {
				onIntegrationsChanged: () => () => {},
			},
			repositories: {
				onDiscoveryCompleted: () => () => {},
				onRepositoriesChanged: () => () => {},
				onRepositoryChanged: () => () => {},
				getRepositoriesState: () =>
					Promise.resolve({ count: 0, openCount: 0, hasUnsafe: false, trusted: true }),
			},
			onboarding: {
				onDidChange: () => () => {},
			},
			ai: {
				onModelChanged: () => () => {},
				onStateChanged: () => () => {},
			},
		} as unknown as Parameters<typeof setupSubscriptions>[1];

		let replaceOverviewCalls = 0;
		const actions = {
			refreshOverview: () => {},
			refreshInactiveOverview: () => {},
			replaceOverview: () => {
				replaceOverviewCalls++;
			},
			updateOverviewFilter: () => {},
			onFocusAccount: () => {},
			onSubscriptionChanged: () => {},
			refreshLaunchpad: () => {},
			refreshAgentOverview: () => {},
			refreshActiveOverview: () => {},
		} satisfies SubscriptionActions;

		const unsubscribe = await setupSubscriptions(state, services, actions);

		assert.ok(onOverviewRepositoryChanged, 'overview repository callback should be registered');
		onOverviewRepositoryChanged?.({ repoPath: '/repo/selected' });

		assert.strictEqual(state.home.overviewRepositoryPath.get(), '/repo/selected');
		assert.strictEqual(replaceOverviewCalls, 1);

		unsubscribe();
	});

	suite('onRepositoryChanged dispatch', () => {
		async function run(
			changes: RepositoryChange[],
			repoPath?: string,
			...overviewRepoPathArg: [overviewRepoPath?: string | undefined]
		): Promise<{ activeCalls: number; inactiveCalls: number; overviewCalls: number }> {
			// Defaults are applied manually so an explicit `undefined` for `overviewRepoPath`
			// is preserved (a `param = default` declaration replaces explicit `undefined` with
			// the default, masking the "no overview repo selected" test case).
			repoPath ??= '/repo/selected';
			const overviewRepoPath = overviewRepoPathArg.length > 0 ? overviewRepoPathArg[0] : '/repo/selected';

			const state = {
				home: createHomeState(new InMemoryStorage()),
				integrations: createIntegrationsState(),
				ai: createAIState(),
				onboarding: createOnboardingState(),
				launchpad: createLaunchpadState(),
				commands: { service: undefined },
			};
			state.home.overviewRepositoryPath.set(overviewRepoPath);

			let onRepositoryChanged: ((event: RepositoryChangeEventData) => void) | undefined;

			const services = {
				home: {
					onWalkthroughProgressChanged: () => () => {},
					onPreviewChanged: () => () => {},
					onAiAllAccessBannerChanged: () => () => {},
					onOverviewRepositoryChanged: () => () => {},
					onOverviewFilterChanged: () => () => {},
					onFocusAccount: () => () => {},
					onAgentSessionsChanged: () => () => {},
				},
				launchpad: { onLaunchpadChanged: () => () => {} },
				config: {},
				subscription: { onSubscriptionChanged: () => () => {} },
				integrations: { onIntegrationsChanged: () => () => {} },
				repositories: {
					onDiscoveryCompleted: () => () => {},
					onRepositoriesChanged: () => () => {},
					onRepositoryChanged: (callback: (event: RepositoryChangeEventData) => void) => {
						onRepositoryChanged = callback;
						return () => {};
					},
					getRepositoriesState: () =>
						Promise.resolve({ count: 0, openCount: 0, hasUnsafe: false, trusted: true }),
				},
				onboarding: { onDidChange: () => () => {} },
				ai: { onModelChanged: () => () => {}, onStateChanged: () => () => {} },
			} as unknown as Parameters<typeof setupSubscriptions>[1];

			let activeCalls = 0;
			let inactiveCalls = 0;
			let overviewCalls = 0;

			const actions = {
				refreshOverview: () => {
					overviewCalls++;
				},
				refreshActiveOverview: () => {
					activeCalls++;
				},
				refreshInactiveOverview: () => {
					inactiveCalls++;
				},
				replaceOverview: () => {},
				updateOverviewFilter: () => {},
				onFocusAccount: () => {},
				onSubscriptionChanged: () => {},
				refreshLaunchpad: () => {},
				refreshAgentOverview: () => {},
			} satisfies SubscriptionActions;

			const unsubscribe = await setupSubscriptions(state, services, actions);

			assert.ok(onRepositoryChanged, 'repository changed callback should be registered');
			onRepositoryChanged?.({ repoPath: repoPath, repoUri: `file://${repoPath}`, changes: changes });

			unsubscribe();
			return { activeCalls: activeCalls, inactiveCalls: inactiveCalls, overviewCalls: overviewCalls };
		}

		test('index → active only', async () => {
			const r = await run(['index']);
			assert.strictEqual(r.activeCalls, 1);
			assert.strictEqual(r.inactiveCalls, 0);
			assert.strictEqual(r.overviewCalls, 0);
		});

		test('pausedOp → active only', async () => {
			const r = await run(['pausedOp']);
			assert.strictEqual(r.activeCalls, 1);
			assert.strictEqual(r.inactiveCalls, 0);
		});

		test('heads → both', async () => {
			const r = await run(['heads']);
			assert.strictEqual(r.activeCalls, 1);
			assert.strictEqual(r.inactiveCalls, 1);
			assert.strictEqual(r.overviewCalls, 0);
		});

		test('worktrees → both', async () => {
			const r = await run(['worktrees']);
			assert.strictEqual(r.activeCalls, 1);
			assert.strictEqual(r.inactiveCalls, 1);
		});

		test('head → both', async () => {
			const r = await run(['head']);
			assert.strictEqual(r.activeCalls, 1);
			assert.strictEqual(r.inactiveCalls, 1);
		});

		test('remotes → both (PR/tracking enrichment may shift)', async () => {
			const r = await run(['remotes']);
			assert.strictEqual(r.activeCalls, 1);
			assert.strictEqual(r.inactiveCalls, 1);
		});

		test('stash → none', async () => {
			const r = await run(['stash']);
			assert.strictEqual(r.activeCalls, 0);
			assert.strictEqual(r.inactiveCalls, 0);
			assert.strictEqual(r.overviewCalls, 0);
		});

		test('config → none', async () => {
			const r = await run(['config']);
			assert.strictEqual(r.activeCalls, 0);
			assert.strictEqual(r.inactiveCalls, 0);
		});

		test('lastFetched → none', async () => {
			const r = await run(['lastFetched']);
			assert.strictEqual(r.activeCalls, 0);
			assert.strictEqual(r.inactiveCalls, 0);
		});

		test('multi-flag [index, head] → both (head escalates)', async () => {
			const r = await run(['index', 'head']);
			assert.strictEqual(r.activeCalls, 1);
			assert.strictEqual(r.inactiveCalls, 1);
		});

		test('multi-flag all-irrelevant → none', async () => {
			const r = await run(['tags', 'config', 'stash']);
			assert.strictEqual(r.activeCalls, 0);
			assert.strictEqual(r.inactiveCalls, 0);
		});

		test('multi-flag [index, config] → active only (config is none)', async () => {
			const r = await run(['index', 'config']);
			assert.strictEqual(r.activeCalls, 1);
			assert.strictEqual(r.inactiveCalls, 0);
		});

		test('event for non-overview repo → no refresh', async () => {
			const r = await run(['heads'], '/repo/other', '/repo/selected');
			assert.strictEqual(r.activeCalls, 0);
			assert.strictEqual(r.inactiveCalls, 0);
		});

		test('no overview repo selected → no refresh', async () => {
			const r = await run(['heads'], '/repo/selected', undefined);
			assert.strictEqual(r.activeCalls, 0);
			assert.strictEqual(r.inactiveCalls, 0);
		});

		test('empty changes array → no refresh', async () => {
			const r = await run([]);
			assert.strictEqual(r.activeCalls, 0);
			assert.strictEqual(r.inactiveCalls, 0);
		});
	});
});
