import { app } from 'electron'; import * as path from 'path'; import { InMemorySessionRepository } from '@/packages/infrastructure/repositories/InMemorySessionRepository'; import { MockBrowserAutomationAdapter, PlaywrightAutomationAdapter, AutomationAdapterMode } from '@/packages/infrastructure/adapters/automation'; import { MockAutomationEngineAdapter } from '@/packages/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter'; import { StartAutomationSessionUseCase } from '@/packages/application/use-cases/StartAutomationSessionUseCase'; import { CheckAuthenticationUseCase } from '@/packages/application/use-cases/CheckAuthenticationUseCase'; import { InitiateLoginUseCase } from '@/packages/application/use-cases/InitiateLoginUseCase'; import { ClearSessionUseCase } from '@/packages/application/use-cases/ClearSessionUseCase'; import { ConfirmCheckoutUseCase } from '@/packages/application/use-cases/ConfirmCheckoutUseCase'; import { loadAutomationConfig, getAutomationMode, AutomationMode, BrowserModeConfigLoader } from '@/packages/infrastructure/config'; import { PinoLogAdapter } from '@/packages/infrastructure/adapters/logging/PinoLogAdapter'; import { NoOpLogAdapter } from '@/packages/infrastructure/adapters/logging/NoOpLogAdapter'; import { loadLoggingConfig } from '@/packages/infrastructure/config/LoggingConfig'; import type { ISessionRepository } from '@/packages/application/ports/ISessionRepository'; import type { IScreenAutomation } from '@/packages/application/ports/IScreenAutomation'; import type { IAutomationEngine } from '@/packages/application/ports/IAutomationEngine'; import type { IAuthenticationService } from '@/packages/application/ports/IAuthenticationService'; import type { ICheckoutConfirmationPort } from '@/packages/application/ports/ICheckoutConfirmationPort'; import type { ILogger } from '@/packages/application/ports/ILogger'; import type { IAutomationLifecycleEmitter } from '@/packages/infrastructure/adapters/IAutomationLifecycleEmitter'; import type { IOverlaySyncPort } from '@/packages/application/ports/IOverlaySyncPort'; import { OverlaySyncService } from '@/packages/application/services/OverlaySyncService'; export interface BrowserConnectionResult { success: boolean; error?: string; } /** * Test-tolerant resolution of the path to store persistent browser session data. * When Electron's `app` is unavailable (e.g., in vitest), fall back to safe defaults. * * @returns Absolute path to the iracing session directory */ import * as os from 'os'; // Use a runtime-safe wrapper around Electron's `app` so importing this module // in a plain Node/Vitest environment does not throw. We intentionally avoid // top-level `app.*` calls without checks. (test-tolerance) let electronApp: { getAppPath?: () => string; getPath?: (name: string) => string; isPackaged?: boolean; } | undefined; try { // Require inside try/catch to avoid module resolution errors in test env. // eslint-disable-next-line @typescript-eslint/no-var-requires const _electron = require('electron'); electronApp = _electron?.app; } catch { electronApp = undefined; } export function resolveSessionDataPath(): string { // Prefer Electron userData if available, otherwise use os.tmpdir() as a safe fallback. const userDataPath = electronApp?.getPath?.('userData') ?? path.join(process.cwd(), 'userData') ?? os.tmpdir(); return path.join(userDataPath, 'iracing-session'); } /** * Resolve the absolute path to the template directory. * Handles both development and production (packaged) Electron environments. * * @returns Absolute path to the iracing templates directory */ export function resolveTemplatePath(): string { // Test-tolerant resolution of template path. Use Electron app when available, // otherwise fall back to process.cwd(). Preserve original runtime behavior when // Electron's app is present (test-tolerance). const appPath = electronApp?.getAppPath?.() ?? process.cwd(); const isPackaged = electronApp?.isPackaged ?? false; if (isPackaged) { return path.join(appPath, 'resources/templates/iracing'); } // Development or unknown environment: prefer project-relative resources. return path.join(appPath, '../../resources/templates/iracing'); } /** * Create logger based on environment configuration. * In test environment, returns NoOpLogAdapter for silent logging. */ function createLogger(): ILogger { const config = loadLoggingConfig(); if (process.env.NODE_ENV === 'test') { return new NoOpLogAdapter(); } return new PinoLogAdapter(config); } /** * Determine the adapter mode based on environment. * - 'production' → 'real' (uses iRacing website selectors) * - 'development' → 'real' (uses iRacing website selectors) * - 'test' → 'mock' (uses data-* attribute selectors) */ function getAdapterMode(envMode: AutomationMode): AutomationAdapterMode { return envMode === 'test' ? 'mock' : 'real'; } /** * Create screen automation adapter based on configuration mode. * * Mode mapping: * - 'production' → PlaywrightAutomationAdapter with mode='real' for iRacing website * - 'development' → PlaywrightAutomationAdapter with mode='real' for iRacing website * - 'test' → MockBrowserAutomationAdapter * * @param mode - The automation mode from configuration * @param logger - Logger instance for the adapter * @returns PlaywrightAutomationAdapter instance (implements both IScreenAutomation and IAuthenticationService) */ function createBrowserAutomationAdapter( mode: AutomationMode, logger: ILogger, browserModeConfigLoader: BrowserModeConfigLoader ): PlaywrightAutomationAdapter | MockBrowserAutomationAdapter { const config = loadAutomationConfig(); // Resolve absolute template path for Electron or fallback environments const absoluteTemplatePath = resolveTemplatePath(); const sessionDataPath = resolveSessionDataPath(); // Use safe accessors for app metadata to avoid throwing in test env (test-tolerance). const safeAppPath = electronApp?.getAppPath?.() ?? process.cwd(); const safeIsPackaged = electronApp?.isPackaged ?? false; logger.debug('Resolved paths', { absoluteTemplatePath, sessionDataPath, appPath: safeAppPath, isPackaged: safeIsPackaged, cwd: process.cwd() }); const adapterMode = getAdapterMode(mode); // Get browser mode configuration from provided loader const browserModeConfig = browserModeConfigLoader.load(); logger.info('Creating browser automation adapter', { envMode: mode, adapterMode, browserMode: browserModeConfig.mode, browserModeSource: browserModeConfig.source, }); switch (mode) { case 'production': case 'development': return new PlaywrightAutomationAdapter( { headless: browserModeConfig.mode === 'headless', mode: adapterMode, userDataDir: sessionDataPath, }, logger.child({ adapter: 'Playwright', mode: adapterMode }), browserModeConfigLoader ); case 'test': default: return new MockBrowserAutomationAdapter(); } } export class DIContainer { private static instance: DIContainer; private logger: ILogger; private sessionRepository!: ISessionRepository; private browserAutomation!: PlaywrightAutomationAdapter | MockBrowserAutomationAdapter; private automationEngine!: IAutomationEngine; private startAutomationUseCase!: StartAutomationSessionUseCase; private checkAuthenticationUseCase: CheckAuthenticationUseCase | null = null; private initiateLoginUseCase: InitiateLoginUseCase | null = null; private clearSessionUseCase: ClearSessionUseCase | null = null; private confirmCheckoutUseCase: ConfirmCheckoutUseCase | null = null; private automationMode: AutomationMode; private browserModeConfigLoader: BrowserModeConfigLoader; private overlaySyncService?: OverlaySyncService; private initialized = false; private constructor() { // Initialize logger first - it's needed by other components this.logger = createLogger(); this.automationMode = getAutomationMode(); this.logger.info('DIContainer initializing', { automationMode: this.automationMode, nodeEnv: process.env.NODE_ENV }); // Defer heavy initialization that may touch Electron/app paths until first use. // Keep BrowserModeConfigLoader available immediately so callers can inspect it. this.browserModeConfigLoader = new BrowserModeConfigLoader(); // Ensure the DIContainer exposes a development-visible default in interactive dev environment. // Some integration/smoke tests expect the DI-provided loader to default to 'headed' in development. if (process.env.NODE_ENV === 'development') { this.browserModeConfigLoader.setDevelopmentMode('headed'); } } /** * Lazily perform initialization that may access Electron APIs or filesystem. * Called on first demand by methods that require the heavy components. */ private ensureInitialized(): void { if (this.initialized) return; const config = loadAutomationConfig(); this.sessionRepository = new InMemorySessionRepository(); this.browserAutomation = createBrowserAutomationAdapter( config.mode, this.logger, this.browserModeConfigLoader ); this.automationEngine = new MockAutomationEngineAdapter( this.browserAutomation, this.sessionRepository ); this.startAutomationUseCase = new StartAutomationSessionUseCase( this.automationEngine, this.browserAutomation, this.sessionRepository ); // Create authentication use cases only for real mode (PlaywrightAutomationAdapter) if (this.browserAutomation instanceof PlaywrightAutomationAdapter) { const authService = this.browserAutomation as IAuthenticationService; this.checkAuthenticationUseCase = new CheckAuthenticationUseCase(authService); this.initiateLoginUseCase = new InitiateLoginUseCase(authService); this.clearSessionUseCase = new ClearSessionUseCase(authService); } else { this.checkAuthenticationUseCase = null; this.initiateLoginUseCase = null; this.clearSessionUseCase = null; } this.logger.info('DIContainer initialized', { automationMode: config.mode, sessionRepositoryType: 'InMemorySessionRepository', browserAutomationType: this.getBrowserAutomationType(config.mode) }); this.initialized = true; } private getBrowserAutomationType(mode: AutomationMode): string { switch (mode) { case 'production': case 'development': return 'PlaywrightAutomationAdapter'; case 'test': default: return 'MockBrowserAutomationAdapter'; } } public static getInstance(): DIContainer { if (!DIContainer.instance) { DIContainer.instance = new DIContainer(); } return DIContainer.instance; } public getStartAutomationUseCase(): StartAutomationSessionUseCase { this.ensureInitialized(); return this.startAutomationUseCase; } public getSessionRepository(): ISessionRepository { this.ensureInitialized(); return this.sessionRepository; } public getAutomationEngine(): IAutomationEngine { this.ensureInitialized(); return this.automationEngine; } public getAutomationMode(): AutomationMode { return this.automationMode; } public getBrowserAutomation(): IScreenAutomation { this.ensureInitialized(); return this.browserAutomation; } public getLogger(): ILogger { return this.logger; } public getCheckAuthenticationUseCase(): CheckAuthenticationUseCase | null { this.ensureInitialized(); return this.checkAuthenticationUseCase; } public getInitiateLoginUseCase(): InitiateLoginUseCase | null { this.ensureInitialized(); return this.initiateLoginUseCase; } public getClearSessionUseCase(): ClearSessionUseCase | null { this.ensureInitialized(); return this.clearSessionUseCase; } public getAuthenticationService(): IAuthenticationService | null { this.ensureInitialized(); if (this.browserAutomation instanceof PlaywrightAutomationAdapter) { return this.browserAutomation as IAuthenticationService; } return null; } public setConfirmCheckoutUseCase( checkoutConfirmationPort: ICheckoutConfirmationPort ): void { this.ensureInitialized(); // Create ConfirmCheckoutUseCase with checkout service from browser automation // and the provided confirmation port this.confirmCheckoutUseCase = new ConfirmCheckoutUseCase( this.browserAutomation as any, // implements ICheckoutService checkoutConfirmationPort ); } public getConfirmCheckoutUseCase(): ConfirmCheckoutUseCase | null { this.ensureInitialized(); return this.confirmCheckoutUseCase; } /** * Initialize automation connection based on mode. * In production/development mode, connects via Playwright browser automation. * In test mode, returns success immediately (no connection needed). */ public async initializeBrowserConnection(): Promise { this.ensureInitialized(); this.logger.info('Initializing automation connection', { mode: this.automationMode }); if (this.automationMode === 'production' || this.automationMode === 'development') { try { const playwrightAdapter = this.browserAutomation as PlaywrightAutomationAdapter; const result = await playwrightAdapter.connect(); if (!result.success) { this.logger.error('Automation connection failed', new Error(result.error || 'Unknown error'), { mode: this.automationMode }); return { success: false, error: result.error }; } this.logger.info('Automation connection established', { mode: this.automationMode, adapter: 'Playwright' }); return { success: true }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Failed to initialize Playwright'; this.logger.error('Automation connection failed', error instanceof Error ? error : new Error(errorMsg), { mode: this.automationMode }); return { success: false, error: errorMsg }; } } this.logger.debug('Test mode - no automation connection needed'); return { success: true }; } /** * Shutdown the container and cleanup resources. * Should be called when the application is closing. */ public async shutdown(): Promise { this.ensureInitialized(); this.logger.info('DIContainer shutting down'); if (this.browserAutomation && 'disconnect' in this.browserAutomation) { try { await (this.browserAutomation as PlaywrightAutomationAdapter).disconnect(); this.logger.info('Automation adapter disconnected'); } catch (error) { this.logger.error('Error disconnecting automation adapter', error instanceof Error ? error : new Error('Unknown error')); } } this.logger.info('DIContainer shutdown complete'); } /** * Get the browser mode configuration loader. * Provides access to runtime browser mode control (headed/headless). */ public getBrowserModeConfigLoader(): BrowserModeConfigLoader { return this.browserModeConfigLoader; } public getOverlaySyncPort(): IOverlaySyncPort { this.ensureInitialized(); if (!this.overlaySyncService) { // Use the browser automation adapter as the lifecycle emitter when available. const lifecycleEmitter = this.browserAutomation as unknown as IAutomationLifecycleEmitter; // Lightweight in-process publisher (best-effort no-op). The ipc handlers will forward lifecycle events to renderer. const publisher = { publish: async (_event: any) => { try { this.logger.debug?.('OverlaySyncPublisher.publish', _event); } catch { // swallow } } } as any; this.overlaySyncService = new OverlaySyncService({ lifecycleEmitter, publisher, logger: this.logger }); } return this.overlaySyncService; } /** * Recreate browser automation and related use-cases from the current * BrowserModeConfigLoader state. This allows runtime changes to the * development-mode headed/headless setting to take effect without * restarting the whole process. */ public refreshBrowserAutomation(): void { this.ensureInitialized(); const config = loadAutomationConfig(); // Recreate browser automation adapter using current loader state this.browserAutomation = createBrowserAutomationAdapter( config.mode, this.logger, this.browserModeConfigLoader ); // Recreate automation engine and start use case to pick up new adapter this.automationEngine = new MockAutomationEngineAdapter( this.browserAutomation, this.sessionRepository ); this.startAutomationUseCase = new StartAutomationSessionUseCase( this.automationEngine, this.browserAutomation, this.sessionRepository ); // Recreate authentication use-cases if adapter supports them, otherwise clear if (this.browserAutomation instanceof PlaywrightAutomationAdapter) { const authService = this.browserAutomation as IAuthenticationService; this.checkAuthenticationUseCase = new CheckAuthenticationUseCase(authService); this.initiateLoginUseCase = new InitiateLoginUseCase(authService); this.clearSessionUseCase = new ClearSessionUseCase(authService); } else { this.checkAuthenticationUseCase = null; this.initiateLoginUseCase = null; this.clearSessionUseCase = null; } this.logger.info('Browser automation refreshed from updated BrowserModeConfigLoader', { browserMode: this.browserModeConfigLoader.load().mode }); } /** * Reset the singleton instance (useful for testing with different configurations). */ public static resetInstance(): void { DIContainer.instance = undefined as unknown as DIContainer; } }