/**
 * Regression Test Runner
 * Runs all backend tests before release
 */

const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const http = require('http');

// Test directory
const TEST_DIR = path.join(__dirname, 'tests');

// API Base URL
const API_BASE = process.env.API_BASE_URL || 'http://localhost:3000';

// Load .env for credential-based tests
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
    const envContent = fs.readFileSync(envPath, 'utf8');
    for (const line of envContent.split('\n')) {
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith('#')) continue;
        const eqIdx = trimmed.indexOf('=');
        if (eqIdx > 0) {
            const key = trimmed.slice(0, eqIdx);
            const val = trimmed.slice(eqIdx + 1);
            if (!process.env[key]) process.env[key] = val;
        }
    }
}

// Test files to run (in order of importance)
// Note: Tests must be v5 compatible (deviceId + entityId based)
const TEST_FILES = [
    // ── Core API tests (no credentials needed) ──
    'test_ux_coverage.js',          // REQUIRED: UX coverage must be >= 98%
    'test_ux_improvements.js',      // Critical: API structure, isBound
    'test_device_isolation.js',     // Critical: Multi-device isolation
    'test_messaging.js',            // Core: Message delivery
    'test_webhook.js',              // Feature: Webhook push
    'test_entity_delete.js',        // Feature: Entity deletion (40 entities stress test)
    'test_widget_ux.js',            // Widget: Chat dialog flow, broadcast
    'test_chat_monitoring.js',      // Chat: Multi-entity speak-to, broadcast, dedup, rate limit
    // test_usage_limit.js removed — daily message limit cancelled (was 15 + invite bonus)
    // test_mission_publish.js removed — relies on legacy todo/add endpoints (now 410)
    'test_entity_name_preservation.js', // Entity name persistence across rebind
    'test_entity_echo_bug.js',      // Echo deduplication regression
    'test_echo_regex_validation.js', // Echo regex validation
    'test_chat_echo_and_delivery.js', // Chat echo and delivery
    'test_entity_xp_preservation.js', // Entity XP preservation across operations
    'test_device_telemetry.js',     // Device telemetry buffer CRUD
    'test_feedback.js',             // Feedback submission and retrieval
    'test_gatekeeper.js',           // Gatekeeper security filter
    'test_entity_management.js',    // Entity management operations

    // ── Static code + API shape tests (no auth credentials needed) ──
    'test-broadcast-recipient-block.js', // Unit: buildBroadcastRecipientBlock() output format
    'test-channel-api.js',          // Channel API: auth rejection, required fields, coexistence
    'test-issue-fixes.js',          // Static code checks (#145/#146-149/#150) + GET /api/skill-templates
    'test-tls-headers.js',          // TLS/Security: HSTS, X-Content-Type-Options, X-Frame-Options
    'test-oidc-foundation.js',      // OIDC: OAuth providers, OIDC validation
    'test-rbac.js',                 // RBAC: Roles and user-roles auth protection
    'test-api-docs.js',             // API Docs: Swagger UI, OpenAPI spec validation
    'test-sdk-generation.js',       // SDK: OpenAPI spec completeness for SDK gen
    'test-grpc-transport.js',       // gRPC: Proto loading, gRPC server, HealthService
    'test-skill-templates.js',      // Skill templates: CRUD, requiredVars format, Gson compat
    'test-ui-text-contrast.js',     // UI: input field text/bg contrast ratio
    'test-portal-duplicate-vars.js', // Portal: no duplicate let/const vs shared scripts
    'test-ux-static-audit.js',      // UX Layer 1: static audit (i18n, form, error, loading, empty, auth, telemetry, a11y, script order)
    'test-ux-parity.js',            // UX Layer 2: cross-platform API-UI parity (Web/Android/iOS)
    'test-ux-live-validation.js',   // UX Layer 3: live endpoint validation (portal pages, auth, API smoke)

    // ── Credential-based tests (require .env with test device credentials) ──
    'test-channel-xp.js',           // Channel XP accumulation: channel-bound entities earn XP via /channel/message (#345)
    'test-channel-e2ee.js',         // Channel E2EE awareness: e2ee_capable flag, encryptionStatus propagation
    'test-broadcast.js',            // Broadcast delivery + delivered_to append (needs BROADCAST_TEST_DEVICE_ID)
    'test-reorder-channel.js',      // Reorder → ENTITY_MOVED channel push (needs BROADCAST_TEST_DEVICE_ID)
    'test-rename-channel.js',       // Rename → NAME_CHANGED channel push (needs BROADCAST_TEST_DEVICE_ID)
    'test-mission-notify-all-types.js', // Mission notify all 4 types to channel bot (needs BROADCAST_TEST_DEVICE_ID)
    'test-mission-notify-channel.js', // Mission notify to channel-bound entities push payload format
    // Note: test-eclaw-context-injection.js excluded (flaky: fails when bot-to-bot rate limit exhausted)
    'test-bot-api-response.js',     // Bot API response rate >= 90% (needs TEST_DEVICE_ID + live bot)
    'test-discord-webhook.js',      // Discord webhook URL detection, rich message structure, content limits
    'test-agent-card-ui.js',        // Agent Card CRUD lifecycle, field validation
    'test-agent-card.js',           // Agent Card: PUT/GET/DELETE lifecycle
    'test-publisher-platforms.js',  // Publisher platforms listing + input validation (12 platforms)
    'test-card-holder.js',          // Card Holder CRUD lifecycle, search, refresh, pin, category, notes
    'test-card-holder-redesign.js', // Card Holder Redesign: my-cards, recent, block/unblock, unified search, chat history
    'test-screen-control-auth.js',  // Screen control portal auth: deviceSecret instead of botSecret
    'test-ai-chat-submit-poll.js',  // AI chat submit/poll async pattern (Issue #248)
    'test-customer-service-api.js', // Customer service AI tools: submit, poll, auth, idempotency
    'test-audit-logging.js',        // Audit logging: GET /api/logs format, category filter
    'test-cross-device-settings.js', // Cross-device settings: CRUD lifecycle, validation, merge
    'test-edit-mode-public-code.js', // Public code: survives entity reorder
    'test-multi-entity-push.js',    // Multi-entity push: POST /api/client/speak with entityId array
    'test-vars-merge.js',           // ENV vars: cross-platform merge, conflict splitting
    'test-ws-auth.js',              // WebSocket: Socket.IO authentication
    'test-ai-chat-image.js',        // AI chat: image support
    'test-ai-chat-webview-guard.js', // AI chat: WebView guard prevents duplicate widget (#419)
    'test-ai-diagnostics.js',       // AI diagnostics: context formatting, injection
    'test-dynamic-entities.js',     // Dynamic entities: add/delete, 20-entity extreme, sparse IDs
    'test-entity-management.js',    // Entity management: refresh cooldown, reorder validation
    'test-entity-cards-stability.js', // Entity cards: don't disappear during polling (#16/#29)
    'test-4th-entity-visibility.js', // 4th entity: shows on home screen after binding (#48)
    'test-channel-e2e.js',          // Channel E2E: binding, plugin isolation, callback routing
    'test-oauth-server.js',         // OAuth 2.0: client registration, tokens, introspection
    'test-a2a-compat.js',           // A2A: .well-known/agent.json, tasks/send
    'test-a2a-task-dispatch.js',    // A2A: official agent sends structured task to entity
    'test-entity-trash.js',         // Entity trash: soft-delete recovery, 7-day retention
    'test-channel-push-text.js',    // Channel push text format verification
    'test-note-pages.js',           // Note page CRUD, public/private toggle, visitor analytics
    'test-invite-auth.js',          // Invite auth: device+cookie dual auth, route conflict fix (#1676)
    'test-org-chart.js',            // Org chart CRUD lifecycle, cycle detection, partial update, auth
    'test-parity-prober.js',        // API-UI parity prober (Layer 4)
    'test-r2-files.js',             // R2 file storage CRUD validation
    'test-r2-quota-rich-card.js',   // R2 quota exceeded rich card E2E
    'test-subscription-plans-live.js', // Subscription plans + wallet live verification
];

// Manual UI tests (run on device, not automated):
// - test_entity_isolation.js     - Manual device UI testing
// - test_entity_communication.js - Manual device UI testing
// - test_name_feature.js         - Manual device UI testing

// Per-test hard cap. Default 120s — enough for every current suite under a
// healthy backend; trips fast when Railway is unreachable instead of hanging
// the release gate for 10 minutes.
//
// Override via env TEST_HARD_CAP_MS (e.g. 600000 for slow stress reruns).
// Per-suite overrides live in TEST_TIMEOUT_OVERRIDES below.
const DEFAULT_TEST_TIMEOUT_MS = 120_000;
const TEST_TIMEOUT = Number.isFinite(Number(process.env.TEST_HARD_CAP_MS)) && Number(process.env.TEST_HARD_CAP_MS) > 0
    ? Number(process.env.TEST_HARD_CAP_MS)
    : DEFAULT_TEST_TIMEOUT_MS;

// Stress tests that legitimately need more than the default cap.
const TEST_TIMEOUT_OVERRIDES = {
    'test_entity_delete.js':       600_000, // 40-entity stress
    'test-dynamic-entities.js':    300_000, // 20-entity extreme + sparse delete cycles
    'test-entity-trash.js':        300_000, // soft-delete + 7-day retention
    'test_ux_coverage.js':         300_000, // coverage aggregation
};

/**
 * Clean up test environment before/after tests
 * Calls /api/debug/reset with deviceSecret
 */
async function cleanupTestEnvironment() {
    return new Promise((resolve) => {
        const debugSecret = process.env.DEBUG_RESET_SECRET || 'test_debug_secret';

        const postData = JSON.stringify({ deviceSecret: debugSecret });

        const options = {
            hostname: new URL(API_BASE).hostname,
            port: new URL(API_BASE).port || 3000,
            path: '/api/debug/reset',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(postData)
            }
        };

        const req = http.request(options, (res) => {
            let data = '';
            res.on('data', (chunk) => data += chunk);
            res.on('end', () => {
                if (res.statusCode === 200) {
                    console.log('🧹 Test environment cleaned up');
                    resolve(true);
                } else {
                    console.log(`⚠️  Cleanup returned status ${res.statusCode}: ${data}`);
                    resolve(false);
                }
            });
        });

        req.on('error', (err) => {
            console.log(`⚠️  Cleanup failed (server may not be running): ${err.message}`);
            resolve(false);
        });

        req.write(postData);
        req.end();
    });
}

async function runTest(testFile) {
    return new Promise((resolve) => {
        const testPath = path.join(TEST_DIR, testFile);

        if (!fs.existsSync(testPath)) {
            console.log(`SKIP: ${testFile} (file not found)`);
            resolve({ file: testFile, passed: true, skipped: true });
            return;
        }

        console.log(`\n${'='.repeat(60)}`);
        console.log(`Running: ${testFile}`);
        console.log('='.repeat(60));

        const child = spawn('node', [testPath], {
            cwd: TEST_DIR,
            env: { ...process.env },
            stdio: 'pipe'
        });

        let output = '';
        let resolved = false;

        // Per-test hard cap. If a test hangs (e.g. bare fetch() against a slow
        // Railway instance) kill the child and emit a clear failure line so the
        // release gate fails in minutes instead of hours.
        const perTestTimeout = TEST_TIMEOUT_OVERRIDES[testFile] || TEST_TIMEOUT;
        const timeout = setTimeout(() => {
            if (!resolved) {
                resolved = true;
                try { child.kill('SIGKILL'); } catch { /* noop */ }
                console.log(`\n⏱️  TEST TIMED OUT: ${testFile} (exceeded ${perTestTimeout / 1000}s hard cap)`);
                resolve({ file: testFile, passed: false, timeout: true, timeoutMs: perTestTimeout });
            }
        }, perTestTimeout);

        child.stdout.on('data', (data) => {
            const str = data.toString();
            output += str;
            process.stdout.write(str);
        });

        child.stderr.on('data', (data) => {
            const str = data.toString();
            output += str;
            process.stderr.write(str);
        });

        child.on('close', (code) => {
            if (resolved) return;
            resolved = true;
            clearTimeout(timeout);

            // Check for various pass patterns
            const passed =
                output.includes('All tests passed') ||
                output.includes('all tests passed') ||
                output.includes('All isolation tests passed') ||
                output.includes('deletion tests passed') ||
                output.includes('webhook registration tests passed') ||
                (output.includes('RESULTS:') && output.includes(', 0 failed')) ||
                (output.includes('Results:') && output.includes('0 failed')) ||
                (output.includes('Result:') && output.includes('passed') && !output.includes('0/')) ||
                (output.match(/\d+\/\d+ tests passed/) && output.includes('✅')) ||
                // Generic: "N passed, 0 failed" — used by channel/issue-fix tests
                /\d+ passed, 0 failed/.test(output) ||
                output.includes('RESULT: ALL PASS') ||       // test-broadcast.js
                output.includes('RESULT: PASS')              // test-bot-api-response.js
            resolve({ file: testFile, passed, code, output });
        });

        child.on('error', (err) => {
            if (resolved) return;
            resolved = true;
            clearTimeout(timeout);
            console.error(`Error running ${testFile}:`, err.message);
            resolve({ file: testFile, passed: false, error: err.message });
        });
    });
}

async function main() {
    console.log('='.repeat(60));
    console.log('REGRESSION TEST RUNNER');
    console.log('='.repeat(60));
    console.log(`Date: ${new Date().toISOString()}`);
    console.log(`Tests to run: ${TEST_FILES.length}`);

    const results = [];

    // Pre-test cleanup
    console.log('\n🧹 Pre-test cleanup...');
    await cleanupTestEnvironment();

    for (const testFile of TEST_FILES) {
        const result = await runTest(testFile);
        results.push(result);
    }

    // Post-test cleanup
    console.log('\n🧹 Post-test cleanup...');
    await cleanupTestEnvironment();

    // Summary
    console.log('\n' + '='.repeat(60));
    console.log('REGRESSION TEST SUMMARY');
    console.log('='.repeat(60));

    let passCount = 0;
    let failCount = 0;
    let skipCount = 0;

    for (const r of results) {
        if (r.skipped) {
            console.log(`⏭️  SKIP: ${r.file}`);
            skipCount++;
        } else if (r.timeout) {
            const s = r.timeoutMs ? ` (${r.timeoutMs / 1000}s cap)` : '';
            console.log(`⏱️  TEST TIMED OUT: ${r.file}${s}`);
            failCount++;
        } else if (r.passed) {
            console.log(`✅ PASS: ${r.file}`);
            passCount++;
        } else {
            console.log(`❌ FAIL: ${r.file}`);
            failCount++;
        }
    }

    console.log('\n' + '-'.repeat(60));
    console.log(`Total: ${passCount} passed, ${failCount} failed, ${skipCount} skipped`);
    console.log('-'.repeat(60));

    if (failCount > 0) {
        console.log('\n❌ RELEASE BLOCKED: Some tests failed!');
        console.log('Fix the failing tests before releasing.');
        process.exit(1);
    } else {
        console.log('\n✅ ALL TESTS PASSED - Ready for release!');
        process.exit(0);
    }
}

main().catch(console.error);
