import { execAsync } from '../../utils.js';
import { execSync } from 'child_process';
import { createModuleLogger } from '../../logger.js';
import { Platform } from '../../types.js';
import { PlatformHandler } from '../../platformHandler.js';
import { existsSync, mkdirSync, rmSync } from 'fs';
import path from 'path';
import { config } from '../../config.js';
import { LogManager } from '../LogManager.js';
import { parseBuildErrors } from '../buildErrorParsing.js';
const logger = createModuleLogger('XcodeBuild');
/**
 * Handles xcodebuild commands for Xcode projects
 */
export class XcodeBuild {
    /**
     * Validates that a scheme supports the requested platform
     * @throws Error if the platform is not supported
     */
    async validatePlatformSupport(projectPath, isWorkspace, scheme, platform) {
        const projectFlag = isWorkspace ? '-workspace' : '-project';
        let command = `xcodebuild -showBuildSettings ${projectFlag} "${projectPath}"`;
        if (scheme) {
            command += ` -scheme "${scheme}"`;
        }
        // Use a generic destination to check platform support
        const destination = PlatformHandler.getGenericDestination(platform);
        command += ` -destination '${destination}'`;
        try {
            logger.debug({ command }, 'Validating platform support');
            // Just check if the command succeeds - we don't need the output
            await execAsync(command, {
                maxBuffer: 1024 * 1024, // 1MB is enough for validation
                timeout: 10000 // 10 second timeout for validation
            });
            logger.debug({ platform, scheme }, 'Platform validation succeeded');
        }
        catch (error) {
            const stderr = error.stderr || '';
            const stdout = error.stdout || '';
            // Check if error indicates platform mismatch
            if (stderr.includes('Available destinations for') || stdout.includes('Available destinations for')) {
                // Extract available platforms from the error message
                const availablePlatforms = this.extractAvailablePlatforms(stderr + stdout);
                throw new Error(`Platform '${platform}' is not supported by scheme '${scheme || 'default'}'. ` +
                    `Available platforms: ${availablePlatforms.join(', ')}`);
            }
            // Some other error - let it pass through for now
            logger.warn({ error: error.message }, 'Platform validation check failed, continuing anyway');
        }
    }
    /**
     * Extracts available platforms from xcodebuild error output
     */
    extractAvailablePlatforms(output) {
        const platforms = new Set();
        const lines = output.split('\n');
        for (const line of lines) {
            // Look for lines like "{ platform:watchOS" or "{ platform:iOS Simulator"
            const match = line.match(/\{ platform:([^,}]+)/);
            if (match) {
                let platform = match[1].trim();
                // Normalize platform names
                if (platform.includes('Simulator')) {
                    platform = platform.replace(' Simulator', '');
                }
                platforms.add(platform);
            }
        }
        return Array.from(platforms);
    }
    /**
     * Build an Xcode project or workspace
     */
    async build(projectPath, isWorkspace, options = {}) {
        const { scheme, configuration = 'Debug', platform = Platform.iOS, deviceId, derivedDataPath = './DerivedData' } = options;
        // Validate platform support first
        await this.validatePlatformSupport(projectPath, isWorkspace, scheme, platform);
        const projectFlag = isWorkspace ? '-workspace' : '-project';
        let command = `xcodebuild ${projectFlag} "${projectPath}"`;
        if (scheme) {
            command += ` -scheme "${scheme}"`;
        }
        command += ` -configuration "${configuration}"`;
        // Determine destination
        let destination;
        if (deviceId) {
            destination = PlatformHandler.getDestination(platform, deviceId);
        }
        else {
            destination = PlatformHandler.getGenericDestination(platform);
        }
        command += ` -destination '${destination}'`;
        command += ` -derivedDataPath "${derivedDataPath}" build`;
        logger.debug({ command }, 'Build command');
        let output = '';
        let exitCode = 0;
        const projectName = path.basename(projectPath, path.extname(projectPath));
        try {
            const { stdout, stderr } = await execAsync(command, {
                maxBuffer: 50 * 1024 * 1024
            });
            output = stdout + (stderr ? `\n${stderr}` : '');
            // Try to find the built app using find command (more reliable than parsing output)
            let appPath;
            try {
                const { stdout: findOutput } = await execAsync(`find "${derivedDataPath}" -name "*.app" -type d | head -1`);
                appPath = findOutput.trim() || undefined;
                if (appPath) {
                    logger.info({ appPath }, 'Found app at path');
                    // Verify the app actually exists
                    if (!existsSync(appPath)) {
                        logger.error({ appPath }, 'App path does not exist!');
                        appPath = undefined;
                    }
                }
                else {
                    logger.warn({ derivedDataPath }, 'No app found in DerivedData');
                }
            }
            catch (error) {
                logger.error({ error: error.message, derivedDataPath }, 'Error finding app path');
            }
            logger.info({ projectPath, scheme, configuration, platform }, 'Build succeeded');
            // Save the build output to logs
            const logPath = LogManager.saveLog('build', output, projectName, {
                scheme,
                configuration,
                platform,
                exitCode,
                command
            });
            return {
                success: true,
                output,
                appPath,
                logPath
            };
        }
        catch (error) {
            logger.error({ error: error.message, projectPath }, 'Build failed');
            output = (error.stdout || '') + (error.stderr ? `\n${error.stderr}` : '');
            exitCode = error.code || 1;
            // Parse compile errors and warnings from output
            const errors = [];
            const lines = output.split('\n');
            const errorRegex = /^(.+?):(\d+):(\d+): (error|warning): (.+)$/;
            const seenErrors = new Set();
            for (const line of lines) {
                const match = line.match(errorRegex);
                if (match) {
                    // Create a unique key for deduplication (same file, line, column, message)
                    const errorKey = `${match[1]}:${match[2]}:${match[3]}:${match[5]}`;
                    if (!seenErrors.has(errorKey)) {
                        seenErrors.add(errorKey);
                        errors.push({
                            file: match[1],
                            line: parseInt(match[2], 10),
                            column: parseInt(match[3], 10),
                            type: match[4],
                            message: match[5]
                        });
                    }
                }
            }
            // Save the build output to logs
            const logPath = LogManager.saveLog('build', output, projectName, {
                scheme,
                configuration,
                platform,
                exitCode,
                command,
                errors
            });
            // Save debug data with parsed errors
            if (errors.length > 0) {
                LogManager.saveDebugData('build-errors', errors, projectName);
            }
            // Also check for non-compile build errors
            const buildErrors = parseBuildErrors(output);
            // Create error with both compile and build errors
            const errorWithDetails = new Error('Build failed');
            errorWithDetails.output = output;
            errorWithDetails.compileErrors = errors; // Compile errors
            errorWithDetails.buildErrors = buildErrors; // Other build errors
            errorWithDetails.logPath = logPath;
            throw errorWithDetails;
        }
    }
    /**
     * Run tests for an Xcode project
     */
    async test(projectPath, isWorkspace, options = {}) {
        const { scheme, configuration = 'Debug', platform = Platform.iOS, deviceId, testFilter, testTarget } = options;
        // Create a unique result bundle path in DerivedData
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const derivedDataPath = config.getDerivedDataPath(projectPath);
        let resultBundlePath = path.join(derivedDataPath, 'Logs', 'Test', `Test-${scheme || 'tests'}-${timestamp}.xcresult`);
        // Ensure result directory exists
        const resultDir = path.dirname(resultBundlePath);
        if (!existsSync(resultDir)) {
            mkdirSync(resultDir, { recursive: true });
        }
        const projectFlag = isWorkspace ? '-workspace' : '-project';
        let command = `xcodebuild ${projectFlag} "${projectPath}"`;
        if (scheme) {
            command += ` -scheme "${scheme}"`;
        }
        command += ` -configuration "${configuration}"`;
        // Determine destination
        let destination;
        if (deviceId) {
            destination = PlatformHandler.getDestination(platform, deviceId);
        }
        else {
            destination = PlatformHandler.getGenericDestination(platform);
        }
        command += ` -destination '${destination}'`;
        // Add test target/filter if provided
        if (testTarget) {
            command += ` -only-testing:${testTarget}`;
        }
        if (testFilter) {
            command += ` -only-testing:${testFilter}`;
        }
        // Disable parallel testing to avoid timeouts and multiple simulator instances
        command += ' -parallel-testing-enabled NO';
        // Add result bundle path
        command += ` -resultBundlePath "${resultBundlePath}"`;
        command += ' test';
        logger.debug({ command }, 'Test command');
        // Use execAsync instead of spawn to ensure the xcresult is fully written when we get the result
        let output = '';
        let code = 0;
        try {
            logger.info('Running tests...');
            const { stdout, stderr } = await execAsync(command, {
                maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large test outputs
                timeout: 600000 // 10 minute timeout for tests
            });
            output = stdout + (stderr ? '\n' + stderr : '');
        }
        catch (error) {
            // Test failure is expected, capture the output
            output = (error.stdout || '') + (error.stderr ? '\n' + error.stderr : '');
            code = error.code || 1;
            logger.debug({ code }, 'Tests completed with failures');
        }
        // Check for compile errors first
        const errorRegex = /^(.+?):(\d+):(\d+): (error|warning): (.+)$/gm;
        const compileErrors = [];
        const seenErrors = new Set();
        let match;
        while ((match = errorRegex.exec(output)) !== null) {
            // Create a unique key for deduplication (same file, line, column, message)
            const errorKey = `${match[1]}:${match[2]}:${match[3]}:${match[5]}`;
            if (!seenErrors.has(errorKey)) {
                seenErrors.add(errorKey);
                compileErrors.push({
                    file: match[1],
                    line: parseInt(match[2], 10),
                    column: parseInt(match[3], 10),
                    type: match[4],
                    message: match[5]
                });
            }
        }
        // Save the full test output to logs
        const projectName = path.basename(projectPath, path.extname(projectPath));
        const logPath = LogManager.saveLog('test', output, projectName, {
            scheme,
            configuration,
            platform,
            exitCode: code,
            command,
            compileErrors: compileErrors.length > 0 ? compileErrors : undefined
        });
        logger.debug({ logPath }, 'Test output saved to log file');
        // Parse the xcresult bundle for accurate test results
        let testResult = {
            passed: 0,
            failed: 0,
            success: false,
            failingTests: undefined,
            logPath
        };
        // Try to extract the actual xcresult path from the output
        const resultMatch = output.match(/Test session results.*?\n\s*(.+\.xcresult)/);
        if (resultMatch) {
            resultBundlePath = resultMatch[1].trim();
            logger.debug({ resultBundlePath }, 'Found xcresult path in output');
        }
        // Also check for the "Writing result bundle at path" message
        const writingMatch = output.match(/Writing result bundle at path:\s*(.+\.xcresult)/);
        if (!resultMatch && writingMatch) {
            resultBundlePath = writingMatch[1].trim();
            logger.debug({ resultBundlePath }, 'Found xcresult path from Writing message');
        }
        try {
            // Check if xcresult exists and wait for it to be fully written
            // Wait for the xcresult bundle to be created and fully written (up to 10 seconds)
            let waitTime = 0;
            const maxWaitTime = 10000;
            const checkInterval = 200;
            // Check both that the directory exists and has the Info.plist file
            const isXcresultReady = () => {
                if (!existsSync(resultBundlePath)) {
                    return false;
                }
                // Check if Info.plist exists inside the bundle, which indicates it's fully written
                const infoPlistPath = path.join(resultBundlePath, 'Info.plist');
                return existsSync(infoPlistPath);
            };
            while (!isXcresultReady() && waitTime < maxWaitTime) {
                await new Promise(resolve => setTimeout(resolve, checkInterval));
                waitTime += checkInterval;
            }
            if (!isXcresultReady()) {
                logger.warn({ resultBundlePath, waitTime }, 'xcresult bundle not ready after waiting, using fallback parsing');
                throw new Error('xcresult bundle not ready');
            }
            // Give xcresulttool a moment to prepare for reading
            await new Promise(resolve => setTimeout(resolve, 300));
            logger.debug({ resultBundlePath, waitTime }, 'xcresult bundle is ready');
            let testReportJson;
            let totalPassed = 0;
            let totalFailed = 0;
            const failingTests = [];
            try {
                // Try the new format first (Xcode 16+)
                logger.debug({ resultBundlePath }, 'Attempting to parse xcresult with new format');
                testReportJson = execSync(`xcrun xcresulttool get test-results summary --path "${resultBundlePath}"`, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
                const summary = JSON.parse(testReportJson);
                logger.debug({ summary: { passedTests: summary.passedTests, failedTests: summary.failedTests } }, 'Got summary from xcresulttool');
                // The summary counts are not reliable for mixed XCTest/Swift Testing
                // We'll count from the detailed test nodes instead
                // Always get the detailed tests to count accurately
                try {
                    const testsJson = execSync(`xcrun xcresulttool get test-results tests --path "${resultBundlePath}"`, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
                    const testsData = JSON.parse(testsJson);
                    // Helper function to count tests and extract failing tests with reasons
                    const processTestNodes = (node, parentName = '') => {
                        if (!node)
                            return;
                        // Count test cases (including argument variations)
                        if (node.nodeType === 'Test Case') {
                            // Check if this test has argument variations
                            let hasArguments = false;
                            if (node.children && Array.isArray(node.children)) {
                                for (const child of node.children) {
                                    if (child.nodeType === 'Arguments') {
                                        hasArguments = true;
                                        // Each argument variation is a separate test
                                        if (child.result === 'Passed') {
                                            totalPassed++;
                                        }
                                        else if (child.result === 'Failed') {
                                            totalFailed++;
                                        }
                                    }
                                }
                            }
                            // If no arguments, count the test case itself
                            if (!hasArguments) {
                                if (node.result === 'Passed') {
                                    totalPassed++;
                                }
                                else if (node.result === 'Failed') {
                                    totalFailed++;
                                    // Extract failure information
                                    let testName = node.nodeIdentifier || node.name || parentName;
                                    let failureReason = '';
                                    // Look for failure message in children
                                    if (node.children && Array.isArray(node.children)) {
                                        for (const child of node.children) {
                                            if (child.nodeType === 'Failure Message') {
                                                failureReason = child.details || child.name || 'Test failed';
                                                break;
                                            }
                                        }
                                    }
                                    // Add test as an object with identifier and reason
                                    failingTests.push({
                                        identifier: testName,
                                        reason: failureReason || 'Test failed (no details available)'
                                    });
                                }
                            }
                        }
                        // Recurse through children
                        if (node.children && Array.isArray(node.children)) {
                            for (const child of node.children) {
                                processTestNodes(child, node.name || parentName);
                            }
                        }
                    };
                    // Parse the test nodes to count tests and extract failing test names with reasons
                    if (testsData.testNodes && Array.isArray(testsData.testNodes)) {
                        for (const testNode of testsData.testNodes) {
                            processTestNodes(testNode);
                        }
                    }
                }
                catch (detailsError) {
                    logger.debug({ error: detailsError.message }, 'Could not extract failing test details');
                }
            }
            catch (newFormatError) {
                // Fall back to legacy format
                logger.debug('Falling back to legacy xcresulttool format');
                testReportJson = execSync(`xcrun xcresulttool get test-report --legacy --format json --path "${resultBundlePath}"`, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
                const testReport = JSON.parse(testReportJson);
                // Parse the legacy test report structure
                if (testReport.tests) {
                    const countTests = (tests) => {
                        for (const test of tests) {
                            if (test.subtests) {
                                // This is a test suite, recurse into it
                                countTests(test.subtests);
                            }
                            else if (test.testStatus) {
                                // This is an actual test
                                if (test.testStatus === 'Success') {
                                    totalPassed++;
                                }
                                else if (test.testStatus === 'Failure' || test.testStatus === 'Expected Failure') {
                                    totalFailed++;
                                    // Extract test name and failure details
                                    if (test.identifier) {
                                        const failureReason = test.failureMessage || test.message || 'Test failed (no details available)';
                                        failingTests.push({
                                            identifier: test.identifier,
                                            reason: failureReason
                                        });
                                    }
                                }
                            }
                        }
                    };
                    countTests(testReport.tests);
                }
            }
            testResult = {
                passed: totalPassed,
                failed: totalFailed,
                success: totalFailed === 0 && code === 0,
                failingTests: failingTests.length > 0 ? failingTests : undefined,
                logPath
            };
            // Save debug data for successful parsing
            LogManager.saveDebugData('test-xcresult-parsed', {
                passed: totalPassed,
                failed: totalFailed,
                failingTests,
                resultBundlePath
            }, projectName);
            logger.info({
                projectPath,
                ...testResult,
                exitCode: code,
                resultBundlePath
            }, 'Tests completed (parsed from xcresult)');
        }
        catch (parseError) {
            logger.error({
                error: parseError.message,
                resultBundlePath,
                xcresultExists: existsSync(resultBundlePath)
            }, 'Failed to parse xcresult bundle');
            // Save debug info about the failure
            LogManager.saveDebugData('test-xcresult-parse-error', {
                error: parseError.message,
                resultBundlePath,
                exists: existsSync(resultBundlePath)
            }, projectName);
            // If xcresulttool fails, try to parse counts from the text output
            const passedMatch = output.match(/Executed (\d+) tests?, with (\d+) failures?/);
            if (passedMatch) {
                const totalTests = parseInt(passedMatch[1], 10);
                const failures = parseInt(passedMatch[2], 10);
                testResult = {
                    passed: totalTests - failures,
                    failed: failures,
                    success: failures === 0,
                    failingTests: undefined,
                    logPath
                };
            }
            else {
                // Last resort fallback
                testResult = {
                    passed: 0,
                    failed: code === 0 ? 0 : 1,
                    success: code === 0,
                    failingTests: undefined,
                    logPath
                };
            }
        }
        // Parse build errors from output
        const buildErrors = parseBuildErrors(output);
        const result = {
            ...testResult,
            success: code === 0 && testResult.failed === 0,
            output,
            compileErrors: compileErrors.length > 0 ? compileErrors : undefined,
            buildErrors: buildErrors.length > 0 ? buildErrors : undefined
        };
        // Clean up the result bundle if tests passed (keep failed results for debugging)
        if (result.success) {
            try {
                rmSync(resultBundlePath, { recursive: true, force: true });
            }
            catch {
                // Ignore cleanup errors
            }
        }
        return result;
    }
    /**
     * Clean build artifacts
     */
    async clean(projectPath, isWorkspace, options = {}) {
        const { scheme, configuration = 'Debug' } = options;
        const projectFlag = isWorkspace ? '-workspace' : '-project';
        let command = `xcodebuild ${projectFlag} "${projectPath}"`;
        if (scheme) {
            command += ` -scheme "${scheme}"`;
        }
        command += ` -configuration "${configuration}" clean`;
        logger.debug({ command }, 'Clean command');
        try {
            await execAsync(command);
            logger.info({ projectPath }, 'Clean succeeded');
        }
        catch (error) {
            logger.error({ error: error.message, projectPath }, 'Clean failed');
            throw new Error(`Clean failed: ${error.message}`);
        }
    }
}
//# sourceMappingURL=XcodeBuild.js.map