// Mosis Designer Test - Automated UI Testing
// Sends input events directly to the designer window using element coordinates from hierarchy dump

#include "test_runner.h"
#include <iostream>
#include <filesystem>
#include <thread>
#include <chrono>
#include <optional>

using namespace mosis::test;

// Helper: Scale coordinates from hierarchy to phone space
// The WindowController expects coordinates in phone space (540x960) and handles DPI scaling internally
void ScaleToPhysical(TestContext& ctx, int& x, int& y) {
    // The hierarchy reports coordinates in RmlUi's context space (540x960)
    // which matches the phone resolution. No scaling needed since
    // WindowController::SendClick handles DPI scaling internally.
    // Just validate the hierarchy dimensions match expected phone size.
    int hierarchyWidth = ctx.hierarchy.GetWidth();
    int hierarchyHeight = ctx.hierarchy.GetHeight();

    if (hierarchyWidth != 540 || hierarchyHeight != 960) {
        std::cerr << "  Warning: Unexpected hierarchy size " << hierarchyWidth << "x" << hierarchyHeight << std::endl;
    }
    // Coordinates stay in phone space (540x960) - no scaling needed
}

// Helper: Click on an element found by ID in the hierarchy
bool ClickById(TestContext& ctx, const std::string& id) {
    ctx.hierarchy.Reload();

    // Debug: Print hierarchy info
    std::cout << "  Hierarchy: " << ctx.hierarchy.GetWidth() << "x" << ctx.hierarchy.GetHeight()
              << ", screen: " << ctx.hierarchy.GetScreenName() << std::endl;

    auto element = ctx.hierarchy.FindById(id);
    if (!element) {
        std::cerr << "  Element not found: #" << id << std::endl;
        // List available IDs for debugging
        auto allElements = ctx.hierarchy.GetAllElements();
        std::cerr << "  Available IDs: ";
        for (const auto& e : allElements) {
            if (!e.id.empty()) {
                std::cerr << "#" << e.id << " ";
            }
        }
        std::cerr << std::endl;
        return false;
    }
    if (!element->visible) {
        std::cerr << "  Element not visible: #" << id << std::endl;
        return false;
    }

    int x = element->bounds.centerX();
    int y = element->bounds.centerY();
    int logicalX = x, logicalY = y;
    ScaleToPhysical(ctx, x, y);

    std::cout << "  Clicking #" << id << " at logical(" << logicalX << "," << logicalY
              << ") -> physical(" << x << "," << y << ")" << std::endl;
    ctx.window.SendClick(x, y);
    return true;
}

// Helper: Click on an element found by class name (first visible match)
bool ClickByClass(TestContext& ctx, const std::string& className) {
    ctx.hierarchy.Reload();
    auto elements = ctx.hierarchy.FindByClass(className);
    for (const auto& element : elements) {
        if (element.visible && element.bounds.width > 0 && element.bounds.height > 0) {
            int x = element.bounds.centerX();
            int y = element.bounds.centerY();
            ScaleToPhysical(ctx, x, y);
            std::cout << "  Clicking ." << className << " at physical(" << x << "," << y << ")" << std::endl;
            ctx.window.SendClick(x, y);
            return true;
        }
    }
    std::cerr << "  No visible element found with class: " << className << std::endl;
    return false;
}

// Helper: Click on an element found by class name at a specific index
bool ClickByClassIndex(TestContext& ctx, const std::string& className, int index) {
    ctx.hierarchy.Reload();
    auto elements = ctx.hierarchy.FindByClass(className);
    int visibleIndex = 0;
    for (const auto& element : elements) {
        if (element.visible && element.bounds.width > 0 && element.bounds.height > 0) {
            if (visibleIndex == index) {
                int x = element.bounds.centerX();
                int y = element.bounds.centerY();
                ScaleToPhysical(ctx, x, y);
                std::cout << "  Clicking ." << className << "[" << index << "] at physical("
                          << x << "," << y << ")" << std::endl;
                ctx.window.SendClick(x, y);
                return true;
            }
            visibleIndex++;
        }
    }
    std::cerr << "  Element ." << className << "[" << index << "] not found (only "
              << visibleIndex << " visible)" << std::endl;
    return false;
}

// Helper: Wait for hierarchy to update to expected screen
bool WaitForScreen(TestContext& ctx, const std::string& expectedScreen, int timeoutMs = 3000) {
    auto startTime = std::chrono::steady_clock::now();
    while (true) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        ctx.hierarchy.Reload();
        std::string currentScreen = ctx.hierarchy.GetScreenName();
        if (currentScreen.find(expectedScreen) != std::string::npos) {
            return true;
        }
        auto elapsed = std::chrono::steady_clock::now() - startTime;
        if (std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() >= timeoutMs) {
            return false;
        }
    }
}

// Helper: Find a back button in the hierarchy
std::optional<ElementInfo> FindBackButton(TestContext& ctx) {
    // Try to find back button by class (app-bar-nav is used in most screens)
    auto elements = ctx.hierarchy.FindByClass("app-bar-nav");
    for (const auto& elem : elements) {
        if (elem.visible && elem.bounds.width > 0 && elem.bounds.height > 0) {
            return elem;
        }
    }

    // Try browser-specific back button
    elements = ctx.hierarchy.FindByClass("browser-nav-btn");
    for (const auto& elem : elements) {
        if (elem.visible && elem.bounds.width > 0 && elem.bounds.height > 0) {
            return elem;
        }
    }

    return std::nullopt;
}

// Helper: Go back to home screen by clicking back button multiple times
void GoHome(TestContext& ctx) {
    // Wait longer for hierarchy file to be updated from any previous navigation
    // The designer writes hierarchy every frame, but there can be a race condition
    std::this_thread::sleep_for(std::chrono::milliseconds(1500));

    for (int i = 0; i < 5; ++i) {
        // Wait then reload to get fresh hierarchy data
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
        ctx.hierarchy.Reload();

        // Check hierarchy to see if dock-phone exists (indicating home screen)
        auto dockPhone = ctx.hierarchy.FindById("dock-phone");
        if (dockPhone && dockPhone->visible) {
            std::cout << "  GoHome: At home screen (dock-phone found)" << std::endl;
            break;
        }

        // Find back button from hierarchy
        auto backBtn = FindBackButton(ctx);
        int x, y;
        if (backBtn) {
            x = backBtn->bounds.centerX();
            y = backBtn->bounds.centerY();
            std::cout << "  GoHome: Found back button at (" << x << "," << y << ")" << std::endl;
        } else {
            // Fallback to default position if no back button found
            x = 48;
            y = 36;
            std::cout << "  GoHome: No back button found, using default position" << std::endl;
        }

        ScaleToPhysical(ctx, x, y);
        std::cout << "  GoHome: Clicking back at (" << x << "," << y << ")" << std::endl;
        ctx.window.SendClick(x, y);

        // Wait for navigation animation to complete
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }

    // Final verification - wait and reload hierarchy
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    ctx.hierarchy.Reload();
    std::cout << "  GoHome: Final screen = " << ctx.hierarchy.GetScreenName() << std::endl;
}

// Test: Navigate to dialer by clicking Phone dock icon
bool TestNavigateToDialer(TestContext& ctx) {
    // Ensure we start from home screen
    GoHome(ctx);
    ctx.log.Clear();

    // Click on Phone dock icon (dock-phone id)
    if (!ClickById(ctx, "dock-phone")) {
        return false;
    }

    // Wait for navigation
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    // Reload log and check for navigation
    ctx.log.Reload();
    return ctx.log.Contains("Loaded screen: apps/dialer/dialer.rml") ||
           ctx.log.Contains("apps/dialer");
}

// Test: Navigate to messages
bool TestNavigateToMessages(TestContext& ctx) {
    // Ensure we start from home screen
    GoHome(ctx);
    ctx.log.Clear();

    // Click on Messages dock icon
    if (!ClickById(ctx, "dock-messages")) {
        return false;
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    ctx.log.Reload();
    return ctx.log.Contains("Loaded screen: apps/messages/messages.rml");
}

// Test: Navigate to contacts
bool TestNavigateToContacts(TestContext& ctx) {
    // Ensure we start from home screen
    GoHome(ctx);
    ctx.log.Clear();

    // Click on Contacts dock icon
    if (!ClickById(ctx, "dock-contacts")) {
        return false;
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    ctx.log.Reload();
    return ctx.log.Contains("Loaded screen: apps/contacts/contacts.rml");
}

// Test: Navigate to browser
bool TestNavigateToBrowser(TestContext& ctx) {
    // Ensure we start from home screen
    GoHome(ctx);
    ctx.log.Clear();

    // Click on Browser dock icon
    if (!ClickById(ctx, "dock-browser")) {
        return false;
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    ctx.log.Reload();
    return ctx.log.Contains("Loaded screen: apps/browser/browser.rml");
}

// Test: Navigate to settings (in app grid, not dock)
bool TestNavigateToSettings(TestContext& ctx) {
    // Ensure we start from home screen
    GoHome(ctx);
    ctx.log.Clear();

    // Settings is in the app grid - find by ID
    if (!ClickById(ctx, "app-settings")) {
        return false;
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    ctx.log.Reload();
    return ctx.log.Contains("Loaded screen: apps/settings/settings.rml");
}

// Test: Multiple navigation sequence from home
bool TestNavigationSequence(TestContext& ctx) {
    // Ensure we start from home screen
    GoHome(ctx);
    ctx.log.Clear();

    // Go to dialer from home
    if (!ClickById(ctx, "dock-phone")) {
        std::cerr << "  Failed to click dock-phone" << std::endl;
        return false;
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    // Go back to home
    GoHome(ctx);

    // Go to messages from home
    if (!ClickById(ctx, "dock-messages")) {
        std::cerr << "  Failed to click dock-messages" << std::endl;
        return false;
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    // Go back to home
    GoHome(ctx);

    // Go to contacts from home
    if (!ClickById(ctx, "dock-contacts")) {
        std::cerr << "  Failed to click dock-contacts" << std::endl;
        return false;
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    ctx.log.Reload();

    // Verify all navigations happened
    bool hasDialer = ctx.log.Contains("apps/dialer");
    bool hasMessages = ctx.log.Contains("apps/messages");
    bool hasContacts = ctx.log.Contains("apps/contacts");

    std::cout << "  Dialer: " << (hasDialer ? "yes" : "no") << std::endl;
    std::cout << "  Messages: " << (hasMessages ? "yes" : "no") << std::endl;
    std::cout << "  Contacts: " << (hasContacts ? "yes" : "no") << std::endl;

    return hasDialer && hasMessages && hasContacts;
}

int main(int argc, char* argv[]) {
    std::cout << "Mosis Designer Test v0.2.0" << std::endl;
    std::cout << "==========================" << std::endl;
    std::cout << "Using UI hierarchy for element coordinates" << std::endl;

    // Determine paths
    // exe is at: designer-test/build/Debug/designer-test.exe
    // project root is: MosisService/
    std::filesystem::path exePath = std::filesystem::absolute(argv[0]).parent_path();
    std::filesystem::path projectRoot = exePath.parent_path().parent_path().parent_path();  // Up 3 levels

    std::filesystem::path designerPath = projectRoot / "designer" / "build" / "Debug" / "mosis-designer.exe";
    std::filesystem::path documentPath = projectRoot / "src" / "main" / "assets" / "apps" / "home" / "home.rml";
    std::filesystem::path logPath = exePath / "designer_test.log";
    std::filesystem::path hierarchyPath = exePath / "ui_hierarchy.json";
    std::filesystem::path resultsPath = exePath / "test_results.json";

    // Allow command-line overrides
    for (int i = 1; i < argc; ++i) {
        std::string arg = argv[i];
        if (arg == "--designer" && i + 1 < argc) {
            designerPath = argv[++i];
        } else if (arg == "--document" && i + 1 < argc) {
            documentPath = argv[++i];
        } else if (arg == "--log" && i + 1 < argc) {
            logPath = argv[++i];
        } else if (arg == "--hierarchy" && i + 1 < argc) {
            hierarchyPath = argv[++i];
        } else if (arg == "--results" && i + 1 < argc) {
            resultsPath = argv[++i];
        } else if (arg == "--help") {
            std::cout << "Usage: designer-test [options]" << std::endl;
            std::cout << "Options:" << std::endl;
            std::cout << "  --designer PATH   Path to mosis-designer.exe" << std::endl;
            std::cout << "  --document PATH   Path to initial RML document" << std::endl;
            std::cout << "  --log PATH        Path for log file" << std::endl;
            std::cout << "  --hierarchy PATH  Path for UI hierarchy dump file" << std::endl;
            std::cout << "  --results PATH    Path for JSON test results" << std::endl;
            std::cout << "  --help            Show this help" << std::endl;
            return 0;
        }
    }

    std::cout << "Designer: " << designerPath << std::endl;
    std::cout << "Document: " << documentPath << std::endl;
    std::cout << "Log file: " << logPath << std::endl;
    std::cout << "Hierarchy: " << hierarchyPath << std::endl;

    // Check if designer exists
    if (!std::filesystem::exists(designerPath)) {
        std::cerr << "ERROR: Designer not found at: " << designerPath << std::endl;
        std::cerr << "Build the designer first: cd designer && cmake --build build --config Debug" << std::endl;
        return 1;
    }

    if (!std::filesystem::exists(documentPath)) {
        std::cerr << "ERROR: Document not found at: " << documentPath << std::endl;
        return 1;
    }

    // Create test runner
    TestRunner runner;
    runner.SetDesignerPath(designerPath);
    runner.SetDocumentPath(documentPath);
    runner.SetLogPath(logPath);
    runner.SetHierarchyPath(hierarchyPath);
    runner.SetStartupTimeoutMs(15000);
    runner.SetActionDelayMs(500);

    // Register tests
    runner.AddTest("Navigate to Dialer", TestNavigateToDialer);
    runner.AddTest("Navigate to Messages", TestNavigateToMessages);
    runner.AddTest("Navigate to Contacts", TestNavigateToContacts);
    runner.AddTest("Navigate to Browser", TestNavigateToBrowser);
    runner.AddTest("Navigation Sequence", TestNavigationSequence);

    // Start designer
    std::cout << "\nStarting designer..." << std::endl;
    if (!runner.StartDesigner()) {
        std::cerr << "Failed to start designer" << std::endl;
        return 1;
    }

    std::cout << "Designer started successfully" << std::endl;
    std::cout << "Window info:" << std::endl;
    const auto& info = runner.GetWindow().GetInfo();
    std::cout << "  Scale: " << info.scaleX << " x " << info.scaleY << std::endl;

    // Run tests
    auto results = runner.RunAll();

    // Save results
    results.SaveToFile(resultsPath);

    // Stop designer
    std::cout << "Stopping designer..." << std::endl;
    runner.StopDesigner();

    // Return exit code based on results
    return (results.failed + results.errors) > 0 ? 1 : 0;
}
