import type { FrameLocator, Locator } from '@playwright/test';
import type { SimulationState } from '../../../src/plus/gk/__debug__accountDebug.js';
import { MaxTimeout, ShortTimeout } from '../baseTest.js';
import { VSCodePage } from './vscodePage.js';

/**
 * Page object for GitLens-specific UI interactions.
 * Extends VSCodePage with GitLens views, commands, and components.
 */
export class GitLensPage extends VSCodePage {
	async isActivated(): Promise<boolean> {
		return this.gitlensTab.isVisible();
	}

	/**
	 * Simulate a subscription state for testing Pro features.
	 * This allows tests to access Pro-gated features like worktrees.
	 *
	 * The debug module is loaded asynchronously after extension activation,
	 * so we retry the command until it succeeds.
	 *
	 * @param state - Subscription state
	 */
	async startSubscriptionSimulation(
		state: SimulationState = { state: 6 /*SubscriptionState.Paid*/, planId: 'pro' },
	): Promise<{ success: boolean } & Disposable> {
		if (!(await this.waitForCommand('gitlens.plus.simulate.subscription'))) {
			throw new Error('gitlens.plus.simulate.subscription command not found');
		}

		const success = await this.executeCommand<boolean>('gitlens.plus.simulate.subscription', state);
		// Wait for the subscription change event to propagate through the system
		await this.page.waitForTimeout(ShortTimeout);
		return {
			success: success,
			[Symbol.dispose]: async () => {
				await this.stopSubscriptionSimulation();
			},
		};
	}

	async stopSubscriptionSimulation(): Promise<void> {
		await this.executeCommand('gitlens.plus.simulate.subscription', { state: null });
	}

	/**
	 * Get the count of GitLens-related tabs in the activity bar
	 * Should be 2: GitLens and GitLens Inspect
	 */
	async getActivityBarTabCount(): Promise<number> {
		return this.activityBar.countTabs(/GitLens/);
	}

	/**
	 * Wait for GitLens extension to fully activate
	 * This is indicated by the GitLens activity bar icon becoming visible
	 */
	async waitForActivation(timeout = MaxTimeout): Promise<void> {
		await this.gitlensTab.waitFor({ state: 'visible', timeout: timeout });
	}

	/** The GitLens activity bar tab */
	get gitlensTab(): Locator {
		return this.activityBar.getTab('GitLens', true);
	}

	/** The GitLens Inspect activity bar tab */
	get gitlensInspectTab(): Locator {
		return this.activityBar.getTab('GitLens Inspect', true);
	}

	/**
	 * Open the GitLens sidebar and ensure it's visible.
	 * Handles the case where the sidebar may be hidden.
	 * Only clicks the tab if it's not already active (to avoid closing it).
	 */
	async openGitLensSidebar(): Promise<void> {
		await this.sidebar.open();
		await this.activityBar.openTab('GitLens', true);
	}

	/**
	 * Open the GitLens Inspect sidebar.
	 * Only clicks the tab if it's not already active (to avoid closing it).
	 */
	async openGitLensInspect(): Promise<void> {
		await this.sidebar.open();
		await this.activityBar.openTab('GitLens Inspect', true);
	}

	// ============================================================================
	// GitLens Sidebar Views
	// ============================================================================

	/** Home section in GitLens sidebar */
	get homeViewSection(): Locator {
		return this.sidebar.getSection(/Home Section/i);
	}

	/** Home webview in GitLens sidebar */
	get homeViewWebview(): Promise<FrameLocator | null> {
		return this.getGitLensWebview('Home', 'webviewView');
	}

	async showHomeView(): Promise<void> {
		await this.executeCommand('gitlens.showHomeView');
	}

	/** Launchpad section in GitLens sidebar */
	get launchpadViewSection(): Locator {
		return this.sidebar.getSection(/Launchpad.*Section/i);
	}

	/** Launchpad tree in GitLens sidebar */
	get launchpadViewTreeView(): Locator {
		return this.sidebar.getTree(/^Launchpad/i);
	}

	async showLaunchpadView(): Promise<void> {
		await this.executeCommand('gitlens.showLaunchpad');
	}

	// ============================================================================
	// GitLens Inspect Views
	// ============================================================================

	/** Inspect section in GitLens Inspect sidebar */
	get inspectViewSection(): Locator {
		return this.sidebar.getSection(/^Inspect/);
	}

	/** Inspect webview in GitLens Inspect sidebar */
	get inspectViewWebview(): Promise<FrameLocator | null> {
		return this.getGitLensWebview('Inspect', 'webviewView');
	}

	/** Line History section in GitLens Inspect sidebar */
	get lineHistoryViewSection(): Locator {
		return this.sidebar.getSection(/^Line History/i);
	}

	/** Line History tree in GitLens Inspect sidebar */
	get lineHistoryViewTreeView(): Locator {
		return this.sidebar.getTree(/^Line History/i);
	}

	async showLineHistoryView(): Promise<void> {
		await this.executeCommand('gitlens.showLineHistoryView');
	}

	/** File History section in GitLens Inspect sidebar */
	get fileHistoryViewSection(): Locator {
		return this.sidebar.getSection(/^File History/i);
	}

	/** File History tree in GitLens Inspect sidebar */
	get fileHistoryViewTreeView(): Locator {
		return this.sidebar.getTree(/^File History/i);
	}

	async showFileHistoryView(): Promise<void> {
		await this.executeCommand('gitlens.showFileHistoryView');
	}

	/** Visual History section in GitLens Inspect sidebar */
	get visualHistoryViewSection(): Locator {
		return this.sidebar.getSection(/^Visual File History/i);
	}

	/** Visual History webview in GitLens Inspect sidebar */
	get visualHistoryViewWebview(): Promise<FrameLocator | null> {
		return this.getGitLensWebview('Visual File History', 'webviewView');
	}

	async showVisualFileHistoryView(): Promise<void> {
		await this.executeCommand('gitlens.showTimelineView');
	}

	/** Search & Compare section in GitLens Inspect sidebar */
	get searchCompareViewSection(): Locator {
		return this.sidebar.getSection(/^Search & Compare/i);
	}

	/** Search & Compare tree in GitLens Inspect sidebar */
	get searchCompareViewTreeView(): Locator {
		return this.sidebar.getTree(/^Search & Compare/i);
	}

	async showSearchAndCompareView(): Promise<void> {
		await this.executeCommand('gitlens.showSearchAndCompareView');
	}

	// ============================================================================
	// GitLens Panel Views (Bottom Panel)
	// ============================================================================

	/** GitLens tab in the bottom panel */
	get gitlensPanel(): Locator {
		return this.panel.getTab('GitLens', true);
	}

	/** Commit Graph view section in the panel (matched via its panel toolbar) */
	get commitGraphViewSection(): Locator {
		// When the Commit Graph view is open, its panel toolbar is labelled
		// "GitLens: Commit Graph: <repo> actions" and stays visible in both the gated (Community)
		// and loaded (Pro) states. Match that toolbar by accessible name — the panel title <h2>
		// itself is sr-hidden, and a bare "Graph" text match resolved to the hidden generic
		// "GitLens: Graph" header.
		return this.panel.locator.getByRole('toolbar', { name: /Commit Graph/ }).first();
	}

	/** Commit Graph webview in the panel */
	get commitGraphViewWebview(): Promise<FrameLocator | null> {
		return this.getGitLensWebview('Graph', 'webviewView');
	}

	async showCommitGraphView(): Promise<void> {
		await this.executeCommand('gitlens.showGraphView');
	}

	/** Commit Graph Details tab in the panel */
	get commitGraphDetailsViewSection(): Locator {
		return this.panel.getTab(/^Graph Details$/i, false);
	}

	/** Commit Graph Details webview in the panel */
	get commitGraphDetailsViewWebview(): Promise<FrameLocator | null> {
		return this.getGitLensWebview('Graph Details', 'webviewView');
	}

	// ============================================================================
	// GitLens Status Bar Items
	// ============================================================================

	/** The "Show Commit Graph" status bar button */
	get commitGraphStatusBarItem(): Locator {
		return this.statusBar.getItem(/Show the GitLens Commit Graph/i);
	}

	/** The Launchpad status bar item */
	get launchpadStatusBarItem(): Locator {
		return this.statusBar.getItem(/GitLens Launchpad/i);
	}

	// ============================================================================
	// GitLens View/WebviewView Commands
	// ============================================================================

	async showGitLensView(): Promise<void> {
		await this.executeCommand('gitlens.views.scm.grouped.focus');
	}

	get gitlensViewSection(): Locator {
		return this.sidebar.getSection(/GitLens/i);
	}

	get gitlensViewTreeView(): Locator {
		return this.sidebar.getTree(/GitLens/i);
	}

	async showCommitsView(): Promise<void> {
		await this.executeCommand('gitlens.showCommitsView');
	}

	async showBranchesView(): Promise<void> {
		await this.executeCommand('gitlens.showBranchesView');
	}

	async showRemotesView(): Promise<void> {
		await this.executeCommand('gitlens.showRemotesView');
	}

	async showStashesView(): Promise<void> {
		await this.executeCommand('gitlens.showStashesView');
	}

	async showTagsView(): Promise<void> {
		await this.executeCommand('gitlens.showTagsView');
	}

	async showWorktreesView(): Promise<void> {
		await this.executeCommand('gitlens.showWorktreesView');
	}

	async showContributorsView(): Promise<void> {
		await this.executeCommand('gitlens.showContributorsView');
	}

	// ============================================================================
	// GitLens Webviews
	// ============================================================================

	// ============================================================================
	// Blame Annotations
	// ============================================================================

	/**
	 * Toggle file blame annotations on the active editor.
	 */
	async toggleFileBlame(): Promise<void> {
		await this.executeCommand('gitlens.toggleFileBlame');
	}

	/**
	 * Check if blame annotations are currently visible in the active editor.
	 *
	 * VS Code renders GitLens gutter blame decorations as CSS `::before`
	 * pseudo-elements on `<span>` elements within `.view-lines`. The decoration
	 * class names contain the `ced-` prefix (content editor decoration).
	 *
	 * @param expectedText — Text to search for in `::before` content (e.g. commit message or date pattern)
	 */
	async hasBlameAnnotations(expectedText: string): Promise<boolean> {
		return this.page.evaluate((pattern: string) => {
			// Blame decorations live on <span> elements inside .view-lines with ced-* classes
			const candidates = document.querySelectorAll('.monaco-editor .view-lines span[class*="ced-"]');
			for (const el of candidates) {
				const content = window.getComputedStyle(el, '::before').getPropertyValue('content');
				if (content && content !== 'none' && content !== '""') {
					const text = content.replace(/^"|"$/g, '');
					if (text.includes(pattern)) return true;
				}
			}
			return false;
		}, expectedText);
	}

	// ============================================================================
	// Webviews
	// ============================================================================

	async getRebaseWebview(): Promise<FrameLocator | null> {
		return this.getGitLensWebview('Interactive Rebase', 'customEditor');
	}

	/** Commit Composer webview (editor panel, id `gitlens.composer`) */
	async getCommitComposerWebview(timeout?: number): Promise<FrameLocator | null> {
		return this.getGitLensWebview('Commit Composer', 'webviewPanel', timeout);
	}

	/**
	 * Get a webview frame locator within a specific parent.
	 * This avoids needing to know specific content inside the webview.
	 *
	 * @param parent - The parent locator to search within
	 * @param timeout - Timeout in ms
	 * @returns A FrameLocator for the webview content, or null if not found
	 */
	async getWebview(parent: Locator, timeout = MaxTimeout / 2): Promise<FrameLocator | null> {
		const startTime = Date.now();
		while (Date.now() - startTime < timeout) {
			const iframes = parent.locator('iframe');
			const count = await iframes.count();

			for (let i = 0; i < count; i++) {
				try {
					const outerFrame = iframes.nth(i).contentFrame();
					const activeFrame = outerFrame.locator('iframe#active-frame');
					if ((await activeFrame.count()) > 0) {
						return activeFrame.contentFrame();
					}
				} catch {
					continue;
				}
			}
			await this.page.waitForTimeout(ShortTimeout);
		}
		return null;
	}

	/**
	 * Find a GitLens webview by its title.
	 * VS Code renders webviews outside their logical containers, so we search all webviews
	 * and identify the correct one by:
	 * 1. The outer iframe src containing extensionId=eamodio.gitlens and purpose=webviewView/webviewPanel
	 * 2. The inner iframe#active-frame having the specified title attribute
	 *
	 * @param title - The title of the webview (e.g., "Graph", "Graph Details", "Home")
	 * @param purpose - The purpose of the webview (e.g., "webviewView", "webviewPanel")
	 * @param timeout - Timeout in ms
	 * @returns A FrameLocator for the matching webview content, or null if not found
	 */
	async getGitLensWebview(
		title: string,
		purpose: 'webviewView' | 'webviewPanel' | 'customEditor',
		timeout = MaxTimeout / 2,
	): Promise<FrameLocator | null> {
		let iterations = 0;
		let usePurpose = true;

		const startTime = Date.now();
		while (Date.now() - startTime < timeout) {
			// Find GitLens webview iframes
			const iframes = this.page.locator(
				usePurpose && purpose === 'webviewView'
					? `iframe.webview[src*="extensionId=eamodio.gitlens"][src*="purpose=${purpose}"]`
					: `iframe.webview[src*="extensionId=eamodio.gitlens"]`,
			);

			iterations++;
			const count = await iframes.count();
			if (count !== 0) {
				for (let i = 0; i < count; i++) {
					try {
						const outerFrame = iframes.nth(i).contentFrame();
						// CSS title*= is a substring match, so do a precise check on the actual
						// title attribute to avoid false positives (e.g. "Graph" matching both
						// "Commit Graph" and "Commit Graph Inspect")
						const activeFrame = outerFrame.locator(`iframe#active-frame[title*="${title}"]`);
						if ((await activeFrame.count()) > 0) {
							const actualTitle = await activeFrame.getAttribute('title');
							// Accept exact match or title with branch suffix, e.g. "Interactive Rebase (main)"
							if (
								actualTitle != null &&
								(actualTitle === title ||
									actualTitle.startsWith(`${title} (`) ||
									actualTitle.startsWith(`${title},`))
							) {
								return activeFrame.contentFrame();
							}
							// Substring matched but title doesn't match precisely — skip
							continue;
						}
					} catch {
						continue;
					}
				}
			} else if (purpose === 'webviewView') {
				// There is a VS Code issue where sometimes WebviewViews don't always get their purpose set (seems to happen after hiding and re-showing)
				// So alternate using purpose filter every other iteration (use it on odd iterations: 1, 3, 5...)
				usePurpose = iterations % 2 === 1;
				continue;
			}
			await this.page.waitForTimeout(ShortTimeout);
		}
		return null;
	}
}
