/**
 * Xcode build and test functionality
 * Single Responsibility: Building and testing projects
 */
import { exec as nodeExec } from 'child_process';
import { promisify } from 'util';
import { existsSync } from 'fs';
import path from 'path';
import { Platform } from './types.js';
import { PlatformHandler } from './platformHandler.js';
import { SimulatorManager } from './simulatorManager.js';
import { createModuleLogger } from './logger.js';
const logger = createModuleLogger('XcodeBuilder');
/**
 * Custom error class for build failures with detailed output
 */
export class BuildError extends Error {
    buildOutput;
    stderr;
    originalError;
    constructor(message, buildOutput = '', stderr = '', originalError) {
        super(message);
        this.name = 'BuildError';
        this.buildOutput = buildOutput;
        this.stderr = stderr;
        this.originalError = originalError;
        // Maintains proper stack trace for where our error was thrown (only available on V8)
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, BuildError);
        }
    }
}
/**
 * XcodeBuilder with dependency injection for testability
 */
export class XcodeBuilder {
    static DEFAULT_TIMEOUT = undefined; // No timeout for builds/tests
    static MAX_BUFFER = 10 * 1024 * 1024; // 10MB
    execAsync;
    // Default instance for static method compatibility
    static defaultInstance;
    constructor(execFunc) {
        // Use provided exec or default to Node's exec
        const exec = execFunc || nodeExec;
        // Proper type assertion for promisified exec
        this.execAsync = promisify(exec);
    }
    /**
     * Get the default instance (singleton pattern)
     */
    static getDefaultInstance() {
        if (!XcodeBuilder.defaultInstance) {
            XcodeBuilder.defaultInstance = new XcodeBuilder();
        }
        return XcodeBuilder.defaultInstance;
    }
    /**
     * Build an Xcode project (instance method)
     * @deprecated Use BuildTool directly instead
     */
    async buildProjectInstance(config) {
        const { projectPath, scheme, platform = Platform.iOS, deviceId, configuration = 'Debug' } = config;
        if (!existsSync(projectPath)) {
            throw new Error(`Project path does not exist: ${projectPath}`);
        }
        // Ensure simulator is booted if needed
        let bootedDevice = '';
        if (PlatformHandler.needsSimulator(platform)) {
            bootedDevice = await SimulatorManager.ensureSimulatorBooted(platform, deviceId);
        }
        const isWorkspace = projectPath.endsWith('.xcworkspace');
        const projectFlag = isWorkspace ? '-workspace' : '-project';
        let command = `xcodebuild ${projectFlag} "${projectPath}"`;
        if (scheme) {
            command += ` -scheme "${scheme}"`;
        }
        command += ` -configuration "${configuration}"`;
        // Add destination
        const destination = PlatformHandler.getDestination(platform, bootedDevice || deviceId);
        command += ` -destination '${destination}'`;
        command += ' -derivedDataPath ./DerivedData build';
        logger.info({ projectPath, scheme, platform, configuration }, 'Building project');
        logger.debug({ command }, 'Build command');
        try {
            const { stdout } = await this.execAsync(command, { maxBuffer: XcodeBuilder.MAX_BUFFER });
            // Try to find the built app
            const appPath = await this.findBuiltAppInstance(projectPath, scheme || 'App', configuration);
            // Note: Installation is now handled by RunProjectTool composing BuildTool + InstallAppTool
            // This method only builds, it doesn't install
            return {
                success: true,
                output: stdout,
                appPath: appPath || undefined
            };
        }
        catch (error) {
            logger.error({ error, projectPath, scheme }, 'Failed to build project');
            // Extract build output from the exec error
            const errorMessage = error.message || 'Unknown build error';
            const stdout = error.stdout || '';
            const stderr = error.stderr || '';
            // Throw a proper BuildError with all the details
            throw new BuildError(`Failed to build project: ${errorMessage}`, stdout, stderr, error);
        }
    }
    /**
     * Test an Xcode project (instance method)
     */
    async testProjectInstance(config) {
        const { projectPath, scheme, platform = Platform.iOS, deviceId, configuration = 'Debug', testTarget, testFilter, parallelTesting = false } = config;
        if (!existsSync(projectPath)) {
            throw new Error(`Project path does not exist: ${projectPath}`);
        }
        // Ensure simulator is booted if needed
        let bootedDevice = '';
        if (PlatformHandler.needsSimulator(platform)) {
            bootedDevice = await SimulatorManager.ensureSimulatorBooted(platform, deviceId);
        }
        const isWorkspace = projectPath.endsWith('.xcworkspace');
        const projectFlag = isWorkspace ? '-workspace' : '-project';
        let command = `xcodebuild test ${projectFlag} "${projectPath}"`;
        if (scheme) {
            command += ` -scheme "${scheme}"`;
        }
        command += ` -configuration "${configuration}"`;
        // Add destination
        const destination = PlatformHandler.getDestination(platform, bootedDevice || deviceId);
        command += ` -destination '${destination}'`;
        // Add test target/filter if provided
        if (testTarget) {
            command += ` -only-testing:${testTarget}`;
        }
        if (testFilter && !testTarget) {
            command += ` -only-testing:${testFilter}`;
        }
        else if (testFilter && testTarget) {
            command += `/${testFilter}`;
        }
        // Add derived data path
        command += ' -derivedDataPath ./DerivedData';
        // Disable parallel testing if requested
        if (!parallelTesting) {
            command += ' -parallel-testing-enabled NO';
        }
        logger.info({ projectPath, scheme, platform, testTarget }, 'Running tests');
        logger.debug({ command }, 'Test command');
        try {
            const { stdout } = await this.execAsync(command, {
                maxBuffer: XcodeBuilder.MAX_BUFFER
                // No timeout - let xcodebuild manage its own timeout
            });
            return XcodeBuilder.parseTestOutput(stdout);
        }
        catch (error) {
            const output = error.stdout || error.message;
            const testResult = XcodeBuilder.parseTestOutput(output);
            // Check if tests ran but some failed (non-zero exit code)
            if (error.stdout && error.stdout.includes('Executed')) {
                return {
                    ...testResult,
                    success: false
                };
            }
            // Build or other error
            logger.error({ error, projectPath, scheme }, 'Failed to run tests');
            throw new Error(`Failed to run tests: ${error.message}`);
        }
    }
    /**
     * Test a Swift Package Manager module (instance method)
     */
    async testSPMModuleInstance(packagePath, platform = Platform.macOS, testFilter, osVersion) {
        if (!existsSync(packagePath)) {
            throw new Error(`Package path does not exist: ${packagePath}`);
        }
        const packageFile = path.join(packagePath, 'Package.swift');
        if (!existsSync(packageFile)) {
            throw new Error(`No Package.swift found at: ${packagePath}`);
        }
        let command;
        if (platform === Platform.macOS) {
            // For macOS, use swift test directly
            command = `swift test --package-path "${packagePath}"`;
            if (testFilter) {
                command += ` --filter "${testFilter}"`;
            }
        }
        else {
            // For other platforms, use xcodebuild
            let destination = PlatformHandler.getDestination(platform);
            if (osVersion) {
                destination = destination.replace(')', `,OS=${osVersion})`);
            }
            command = `xcodebuild test -scheme "$(xcodebuild -list -json | jq -r '.project.schemes[0]')" -destination '${destination}' -enableCodeCoverage YES`;
            if (testFilter) {
                command += ` -only-testing:${testFilter}`;
            }
        }
        logger.info({ packagePath, platform, testFilter }, 'Testing SPM module');
        logger.debug({ command }, 'SPM test command');
        try {
            const { stdout } = await this.execAsync(command, {
                cwd: packagePath,
                maxBuffer: XcodeBuilder.MAX_BUFFER
            });
            return XcodeBuilder.parseTestOutput(stdout);
        }
        catch (error) {
            const output = error.stdout || error.message;
            return {
                ...XcodeBuilder.parseTestOutput(output),
                success: false,
                errors: error.message
            };
        }
    }
    /**
     * Find the built app in DerivedData (instance method)
     */
    async findBuiltAppInstance(projectPath, scheme, configuration) {
        try {
            const derivedDataPath = './DerivedData';
            const { stdout } = await this.execAsync(`find "${derivedDataPath}" -name "*.app" -type d | head -1`);
            return stdout.trim() || null;
        }
        catch {
            return null;
        }
    }
    /**
     * Clean build artifacts, DerivedData, or test results
     */
    // Deprecated - use CleanBuildTool directly
    async cleanProjectInstance(config) {
        logger.warn('cleanProjectInstance is deprecated. Use CleanBuildTool directly.');
        // For backward compatibility, delegate to CleanBuildTool
        const { CleanBuildTool } = await import('./tools/CleanBuildTool.js');
        const cleanTool = new CleanBuildTool();
        const result = await cleanTool.execute(config);
        // Parse the result to match the expected return type
        const parsed = JSON.parse(result.content[0].text);
        return parsed;
    }
    /**
     * List available schemes in a project
     */
    async listSchemesInstance(projectPath, shared = true) {
        try {
            logger.info({ projectPath, shared }, 'Listing schemes');
            const projectType = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project';
            const projectFlag = projectType === 'workspace' ? '-workspace' : '-project';
            const { stdout } = await this.execAsync(`xcodebuild -list ${projectFlag} "${projectPath}" -json`);
            const data = JSON.parse(stdout);
            const schemes = [];
            // Get schemes from the project/workspace
            if (data.project?.schemes) {
                schemes.push(...data.project.schemes);
            }
            if (data.workspace?.schemes) {
                schemes.push(...data.workspace.schemes);
            }
            logger.info({ schemes: schemes.length }, 'Found schemes');
            return schemes;
        }
        catch (error) {
            logger.error({ error, projectPath }, 'Failed to list schemes');
            throw new Error(`Failed to list schemes: ${error.message}`);
        }
    }
    /**
     * Get build settings for a scheme
     */
    async getBuildSettingsInstance(projectPath, scheme, configuration, platform) {
        try {
            logger.info({ projectPath, scheme, configuration, platform }, 'Getting build settings');
            const projectType = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project';
            const projectFlag = projectType === 'workspace' ? '-workspace' : '-project';
            let command = `xcodebuild -showBuildSettings ${projectFlag} "${projectPath}" -scheme "${scheme}"`;
            if (configuration) {
                command += ` -configuration "${configuration}"`;
            }
            if (platform) {
                const destination = PlatformHandler.getDestination(platform);
                command += ` -destination "${destination}"`;
            }
            const { stdout } = await this.execAsync(command);
            // Parse build settings
            const settings = {};
            const lines = stdout.split('\n');
            for (const line of lines) {
                const match = line.match(/^\s{4}(\w+)\s*=\s*(.*)$/);
                if (match) {
                    settings[match[1]] = match[2];
                }
            }
            logger.info({ settingsCount: Object.keys(settings).length }, 'Retrieved build settings');
            return settings;
        }
        catch (error) {
            logger.error({ error, projectPath, scheme }, 'Failed to get build settings');
            throw new Error(`Failed to get build settings: ${error.message}`);
        }
    }
    /**
     * Get project information (bundle ID, version, etc.)
     */
    async getProjectInfoInstance(projectPath) {
        try {
            logger.info({ projectPath }, 'Getting project info');
            const projectType = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project';
            const projectFlag = projectType === 'workspace' ? '-workspace' : '-project';
            // Get list output in JSON format
            const { stdout } = await this.execAsync(`xcodebuild -list ${projectFlag} "${projectPath}" -json`);
            const data = JSON.parse(stdout);
            const info = {
                name: data.project?.name || data.workspace?.name,
                schemes: data.project?.schemes || data.workspace?.schemes || [],
                targets: data.project?.targets || [],
                configurations: data.project?.configurations || []
            };
            logger.info({ info }, 'Retrieved project info');
            return info;
        }
        catch (error) {
            logger.error({ error, projectPath }, 'Failed to get project info');
            throw new Error(`Failed to get project info: ${error.message}`);
        }
    }
    /**
     * List targets in a project
     */
    async listTargetsInstance(projectPath) {
        try {
            logger.info({ projectPath }, 'Listing targets');
            const projectType = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project';
            const projectFlag = projectType === 'workspace' ? '-workspace' : '-project';
            const { stdout } = await this.execAsync(`xcodebuild -list ${projectFlag} "${projectPath}" -json`);
            const data = JSON.parse(stdout);
            const targets = data.project?.targets || [];
            logger.info({ targets: targets.length }, 'Found targets');
            return targets;
        }
        catch (error) {
            logger.error({ error, projectPath }, 'Failed to list targets');
            throw new Error(`Failed to list targets: ${error.message}`);
        }
    }
    /**
     * Archive a project
     */
    async archiveProjectInstance(projectPath, scheme, platform, configuration, archivePath) {
        try {
            logger.info({ projectPath, scheme, platform, configuration }, 'Archiving project');
            const projectType = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project';
            const projectFlag = projectType === 'workspace' ? '-workspace' : '-project';
            // Default archive path if not specified
            const finalArchivePath = archivePath ||
                `${process.env.HOME}/Library/Developer/Xcode/Archives/${new Date().toISOString().split('T')[0]}/${scheme}.xcarchive`;
            let command = `xcodebuild archive ${projectFlag} "${projectPath}" -scheme "${scheme}" -configuration "${configuration}" -archivePath "${finalArchivePath}"`;
            // Add platform-specific destination
            const destination = PlatformHandler.getDestination(platform);
            command += ` -destination "${destination}"`;
            const { stdout } = await this.execAsync(command, { maxBuffer: 50 * 1024 * 1024 });
            logger.info({ archivePath: finalArchivePath }, 'Archive created successfully');
            return finalArchivePath;
        }
        catch (error) {
            logger.error({ error, projectPath, scheme }, 'Failed to archive project');
            throw new Error(`Failed to archive project: ${error.message}`);
        }
    }
    /**
     * Export IPA from archive
     */
    async exportIPAInstance(archivePath, exportPath, exportMethod = 'development') {
        try {
            logger.info({ archivePath, exportMethod }, 'Exporting IPA');
            const finalExportPath = exportPath || `${process.cwd()}/Export`;
            // Create export options plist
            const exportOptionsPlist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>${exportMethod}</string>
    <key>compileBitcode</key>
    <false/>
</dict>
</plist>`;
            // Write export options to temp file
            const fs = require('fs');
            const tempPlistPath = `/tmp/exportOptions-${Date.now()}.plist`;
            fs.writeFileSync(tempPlistPath, exportOptionsPlist);
            try {
                const { stdout } = await this.execAsync(`xcodebuild -exportArchive -archivePath "${archivePath}" -exportPath "${finalExportPath}" -exportOptionsPlist "${tempPlistPath}"`);
                // Clean up temp file
                fs.unlinkSync(tempPlistPath);
                logger.info({ exportPath: finalExportPath }, 'IPA exported successfully');
                return finalExportPath;
            }
            catch (error) {
                // Clean up temp file on error
                fs.unlinkSync(tempPlistPath);
                throw error;
            }
        }
        catch (error) {
            logger.error({ error, archivePath }, 'Failed to export IPA');
            throw new Error(`Failed to export IPA: ${error.message}`);
        }
    }
    /**
     * Parse test output to extract results (static for easy testing)
     */
    static parseTestOutput(output) {
        const lines = output.split('\n');
        let testCount = 0;
        let failureCount = 0;
        let success = false;
        for (const line of lines) {
            // XCTest framework patterns
            if (line.includes('Test Suite') && line.includes('passed')) {
                success = true;
            }
            if (line.includes('Test Suite') && line.includes('failed')) {
                success = false;
            }
            // XCTest: "Executed N tests"
            const testMatch = line.match(/Executed (\d+) test/);
            if (testMatch) {
                testCount = parseInt(testMatch[1]);
            }
            // Swift Testing framework patterns (new framework in Xcode 16)
            // "✔ Test run with N test(s) passed"
            const swiftTestMatch = line.match(/Test run with (\d+) test/);
            if (swiftTestMatch) {
                testCount = parseInt(swiftTestMatch[1]);
                if (line.includes('passed')) {
                    success = true;
                }
            }
            // XCTest failures
            const failureMatch = line.match(/(\d+) failure[s]?/);
            if (failureMatch) {
                failureCount = parseInt(failureMatch[1]);
            }
            // Swift Testing failures (look for ✗ symbol on individual test lines, not summary)
            if (line.includes('✗ Test') && line.includes('() failed')) {
                failureCount++;
            }
        }
        return {
            success,
            output, // Return FULL output - crucial for debugging in VS Code
            testCount,
            failureCount
        };
    }
    // Static methods for backward compatibility - delegate to default instance
    /**
     * @deprecated Use BuildTool directly instead
     */
    static async buildProject(config) {
        return XcodeBuilder.getDefaultInstance().buildProjectInstance(config);
    }
    static async testProject(config) {
        return XcodeBuilder.getDefaultInstance().testProjectInstance(config);
    }
    static async testSPMModule(packagePath, platform = Platform.macOS, testFilter, osVersion) {
        return XcodeBuilder.getDefaultInstance().testSPMModuleInstance(packagePath, platform, testFilter, osVersion);
    }
    static async cleanProject(config) {
        return XcodeBuilder.getDefaultInstance().cleanProjectInstance(config);
    }
}
//# sourceMappingURL=xcodeBuilder.js.map