// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
// SPDX-License-Identifier: GPL-3.0-or-later

#include "Tests/Integration/TestMainWindowGui.h"

#include <QAction>
#include <QApplication>
#include <QFile>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QScrollBar>
#include <QShortcut>
#include <QSlider>
#include <QTabWidget>
#include <QTest>
#include <QTimer>
#include <QWheelEvent>

#include "App/Core/Application.h"
#include "App/Core/Settings.h"
#include "App/Core/ThemeManager.h"
#include "App/Element/GraphicElements/And.h"
#include "App/Element/GraphicElements/Clock.h"
#include "App/Element/GraphicElements/InputButton.h"
#include "App/Element/GraphicElements/InputSwitch.h"
#include "App/Element/GraphicElements/Led.h"
#include "App/Element/GraphicElements/Node.h"
#include "App/Element/GraphicElements/Or.h"
#include "App/Element/IC.h"
#include "App/Element/ICRegistry.h"
#include "App/IO/Serialization.h"
#include "App/Nodes/QNEConnection.h"
#include "App/Nodes/QNEPort.h"
#include "App/Scene/Commands.h"
#include "App/Scene/GraphicsView.h"
#include "App/Scene/Scene.h"
#include "App/Scene/Workspace.h"
#include "App/Simulation/Simulation.h"
#include "App/UI/ElementEditor.h"
#include "App/UI/ElementPalette.h"
#include "App/UI/FileDialogProvider.h"
#include "App/UI/MainWindow.h"
#include "App/UI/TrashButton.h"
#include "Tests/Common/ICTestHelpers.h"
#include "Tests/Common/StubFileDialogProvider.h"
#include "Tests/Common/TestUtils.h"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

namespace MainWindowGuiHelpers {

static void createMWFixture(const QString &path)
{
    WorkSpace ws;
    auto *sw = new InputSwitch();
    auto *led = new Led();
    ws.scene()->addItem(sw);
    ws.scene()->addItem(led);
    ws.save(path);
}

static QTabWidget *findTabs(QWidget *w)
{
    return w->findChild<QTabWidget *>("tab");
}

/// Creates a shown, focused MainWindow ready for keyboard/mouse input.
/// Forces English locale so that translated keyboard shortcuts match QTest key events.
/// Uses QApplication::setActiveWindow (deprecated but required for offscreen platform
/// where activateWindow() is a no-op since there is no window manager).
static MainWindow *createMW()
{
    Settings::setLanguage("en");
    auto *w = new MainWindow();
    w->show();
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
    QApplication::setActiveWindow(w);
QT_WARNING_POP
    return w;
}

/// Sends a mouse click at the scene position of an element.
static void clickElement(GraphicsView *view, GraphicElement *elm, Qt::KeyboardModifiers mods = Qt::NoModifier)
{
    QPoint viewPos = view->mapFromScene(elm->scenePos());
    QTest::mouseClick(view->viewport(), Qt::LeftButton, mods, viewPos);
}

/// Sends a mouse click at a scene position.
static void clickScene(GraphicsView *view, QPointF scenePos)
{
    QPoint viewPos = view->mapFromScene(scenePos);
    QTest::mouseClick(view->viewport(), Qt::LeftButton, Qt::NoModifier, viewPos);
}

/// Sends a mouse drag from startScene to endScene in incremental steps.
static void dragElement(GraphicsView *view, QPointF startScene, QPointF endScene, int steps = 5)
{
    QPoint start = view->mapFromScene(startScene);
    QPoint end = view->mapFromScene(endScene);
    QPoint delta = (end - start) / steps;

    QTest::mousePress(view->viewport(), Qt::LeftButton, Qt::NoModifier, start);
    for (int i = 1; i <= steps; ++i) {
        QPoint pos = start + delta * i;
        QMouseEvent move(QEvent::MouseMove, pos, view->viewport()->mapToGlobal(pos),
                         Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
        QApplication::sendEvent(view->viewport(), &move);
    }
    QTest::mouseRelease(view->viewport(), Qt::LeftButton, Qt::NoModifier, end);
}

/// Sends a mouse wheel event with Ctrl held for zooming.
static void wheelZoom(GraphicsView *view, int delta)
{
    QPoint center = view->viewport()->rect().center();
    QWheelEvent wheel(QPointF(center), view->viewport()->mapToGlobal(center),
                      QPoint(0, 0), QPoint(0, delta),
                      Qt::NoButton, Qt::ControlModifier,
                      Qt::NoScrollPhase, false);
    QApplication::sendEvent(view->viewport(), &wheel);
}

/// Schedules auto-close of the next visible QMessageBox that appears.
static void autoCloseNextMessageBox()
{
    QTimer::singleShot(0, [] {
        if (auto *w = QApplication::activeModalWidget()) {
            if (auto *msgBox = qobject_cast<QMessageBox *>(w)) {
                msgBox->accept();
            }
        }
    });
}

static void clickNextMessageBoxButton(const QString &text)
{
    QTimer::singleShot(0, [text] {
        if (auto *w = QApplication::activeModalWidget()) {
            if (auto *msgBox = qobject_cast<QMessageBox *>(w)) {
                for (auto *btn : msgBox->buttons()) {
                    if (btn->text() == text) {
                        btn->click();
                        return;
                    }
                }
            }
        }
    });
}

} // namespace MainWindowGuiHelpers

using namespace MainWindowGuiHelpers;

// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------

void TestMainWindowGui::initTestCase()
{
    QVERIFY(m_tempDir.isValid());
    m_fixtureDir = m_tempDir.path();
    createMWFixture(m_fixtureDir + "/test_circuit.panda");
}

void TestMainWindowGui::cleanup()
{
    FileDialogs::setProvider(nullptr);
    QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
    QCoreApplication::processEvents();
}

// ===========================================================================
// Tab management
// ===========================================================================

void TestMainWindowGui::testNewTabCreated()
{
    std::unique_ptr<MainWindow> window(createMW());
    QVERIFY(window->currentTab() != nullptr);

    auto *tabs = findTabs(window.get());
    QVERIFY(tabs);
    QCOMPARE(tabs->count(), 1);
}

void TestMainWindowGui::testCreateMultipleTabs()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());
    QCOMPARE(tabs->count(), 1);

    QTest::keyClick(window.get(), Qt::Key_N, Qt::ControlModifier);
    QCOMPARE(tabs->count(), 2);

    QTest::keyClick(window.get(), Qt::Key_N, Qt::ControlModifier);
    QCOMPARE(tabs->count(), 3);
}

void TestMainWindowGui::testCloseTabReducesCount()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());

    QTest::keyClick(window.get(), Qt::Key_N, Qt::ControlModifier);
    QCOMPARE(tabs->count(), 2);

    tabs->tabCloseRequested(tabs->count() - 1);
    QCOMPARE(tabs->count(), 1);
}

void TestMainWindowGui::testSwitchTab()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());

    auto *firstTab = window->currentTab();
    QTest::keyClick(window.get(), Qt::Key_N, Qt::ControlModifier);
    auto *secondTab = window->currentTab();
    QVERIFY(firstTab != secondTab);

    tabs->setCurrentIndex(0);
    QCOMPARE(window->currentTab(), firstTab);

    tabs->setCurrentIndex(1);
    QCOMPARE(window->currentTab(), secondTab);
}

void TestMainWindowGui::testCurrentTabReturnsActiveWorkspace()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tab = window->currentTab();
    QVERIFY(tab != nullptr);
    QVERIFY(tab->scene() != nullptr);
    QVERIFY(tab->simulation() != nullptr);
}

// ===========================================================================
// File operations
// ===========================================================================

void TestMainWindowGui::testOpenFile()
{
    std::unique_ptr<MainWindow> window(createMW());

    ScopedFileDialogStub guard;
    guard.stub.openResult = m_fixtureDir + "/test_circuit.panda";

    QTest::keyClick(window.get(), Qt::Key_O, Qt::ControlModifier);

    QCOMPARE(guard.stub.openCallCount, 1);
    QVERIFY(!window->currentTab()->scene()->elements().isEmpty());
}

void TestMainWindowGui::testOpenCreatesNewTab()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());
    int countBefore = tabs->count();

    ScopedFileDialogStub guard;
    guard.stub.openResult = m_fixtureDir + "/test_circuit.panda";
    QTest::keyClick(window.get(), Qt::Key_O, Qt::ControlModifier);

    QCOMPARE(tabs->count(), countBefore + 1);
}

void TestMainWindowGui::testSaveAndReload()
{
    const QString savePath = m_fixtureDir + "/save_reload.panda";

    {
        std::unique_ptr<MainWindow> window(createMW());
        auto *scene = window->currentTab()->scene();
        scene->addItem(new InputSwitch());
        scene->addItem(new And());

        ScopedFileDialogStub guard;
        guard.stub.saveResult = {savePath, "Panda files (*.panda)"};
        QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);
        QVERIFY(QFile::exists(savePath));
    }

    {
        std::unique_ptr<MainWindow> window(createMW());
        ScopedFileDialogStub guard;
        guard.stub.openResult = savePath;
        QTest::keyClick(window.get(), Qt::Key_O, Qt::ControlModifier);

        QVERIFY(window->currentTab()->scene()->elements().size() >= 2);
    }

    QFile::remove(savePath);
}

void TestMainWindowGui::testSaveAsConflictBlocksSave()
{
    QTemporaryDir tmpDir;
    QVERIFY(tmpDir.isValid());
    const QString path1 = tmpDir.path() + "/file_a.panda";
    const QString path2 = tmpDir.path() + "/file_b.panda";
    createMWFixture(path1);
    createMWFixture(path2);

    std::unique_ptr<MainWindow> window(createMW());
    window->loadPandaFile(path1);
    window->loadPandaFile(path2);

    // Active tab is path2; attempt Save As into path1 which is open in another tab.
    ScopedFileDialogStub guard;
    guard.stub.saveResult = {path1, "Panda files (*.panda)"};

    autoCloseNextMessageBox(); // dismiss conflict dialog with OK
    QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier | Qt::ShiftModifier);

    // Save was blocked: active tab is still path2.
    QCOMPARE(window->currentFile().absoluteFilePath(), QFileInfo(path2).absoluteFilePath());
}

void TestMainWindowGui::testSaveAsConflictSwitchToTab()
{
    QTemporaryDir tmpDir;
    QVERIFY(tmpDir.isValid());
    const QString path1 = tmpDir.path() + "/file_a.panda";
    const QString path2 = tmpDir.path() + "/file_b.panda";
    createMWFixture(path1);
    createMWFixture(path2);

    std::unique_ptr<MainWindow> window(createMW());
    window->loadPandaFile(path1);
    window->loadPandaFile(path2);

    // Active tab is path2; attempt Save As into path1 — a conflict.
    ScopedFileDialogStub guard;
    guard.stub.saveResult = {path1, "Panda files (*.panda)"};

    clickNextMessageBoxButton("Switch to Tab");
    QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier | Qt::ShiftModifier);

    // "Switch to Tab" was clicked: the tab for path1 is now active.
    QCOMPARE(window->currentFile().absoluteFilePath(), QFileInfo(path1).absoluteFilePath());
}

void TestMainWindowGui::testReloadFile()
{
    std::unique_ptr<MainWindow> window(createMW());

    ScopedFileDialogStub guard;
    guard.stub.openResult = m_fixtureDir + "/test_circuit.panda";
    QTest::keyClick(window.get(), Qt::Key_O, Qt::ControlModifier);

    QVERIFY(window->currentFile().exists());
    auto *scene = window->currentTab()->scene();
    int elemsBefore = static_cast<int>(scene->elements().size());

    scene->addItem(new And());
    QCOMPARE(static_cast<int>(scene->elements().size()), elemsBefore + 1);

    QTest::keyClick(window.get(), Qt::Key_F5, Qt::ControlModifier);

    QCOMPARE(static_cast<int>(window->currentTab()->scene()->elements().size()), elemsBefore);
}

// ===========================================================================
// Simulation controls
// ===========================================================================

void TestMainWindowGui::testPlayPauseToggle()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *sim = window->currentTab()->simulation();
    bool initialRunning = sim->isRunning();

    QTest::keyClick(window.get(), Qt::Key_F5);
    QVERIFY(sim->isRunning() != initialRunning);

    QTest::keyClick(window.get(), Qt::Key_F5);
    QCOMPARE(sim->isRunning(), initialRunning);
}

void TestMainWindowGui::testRestartSimulation()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    auto *sw = new InputSwitch();
    auto *led = new Led();
    scene->addItem(sw);
    scene->addItem(led);

    CircuitBuilder builder(scene);
    builder.connect(sw, 0, led, 0);
    auto *sim = builder.initSimulation();
    sw->setOn(true);
    sim->update();
    QCOMPARE(TestUtils::getInputStatus(led), true);

    auto *action = window->findChild<QAction *>("actionRestart");
    QVERIFY2(action, "actionRestart not found");
    action->trigger();
    QVERIFY(window->currentTab()->simulation() != nullptr);
}

// ===========================================================================
// View toggles
// ===========================================================================

void TestMainWindowGui::testToggleWiresVisibility()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *action = window->findChild<QAction *>("actionWires");
    QVERIFY(action);
    bool initial = action->isChecked();

    QTest::keyClick(window.get(), Qt::Key_1, Qt::ControlModifier);
    QVERIFY(action->isChecked() != initial);

    QTest::keyClick(window.get(), Qt::Key_1, Qt::ControlModifier);
    QCOMPARE(action->isChecked(), initial);
}

void TestMainWindowGui::testToggleGatesVisibility()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *action = window->findChild<QAction *>("actionGates");
    QVERIFY(action);
    bool initial = action->isChecked();

    QTest::keyClick(window.get(), Qt::Key_2, Qt::ControlModifier);
    QVERIFY(action->isChecked() != initial);

    QTest::keyClick(window.get(), Qt::Key_2, Qt::ControlModifier);
    QCOMPARE(action->isChecked(), initial);
}

void TestMainWindowGui::testZoomInOutReset()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *view = window->currentTab()->view();

    QTest::keyClick(window.get(), Qt::Key_0, Qt::ControlModifier);
    QTransform baseTx = view->transform();

    QTest::keyClick(window.get(), Qt::Key_Equal, Qt::ControlModifier);
    QVERIFY(view->transform().m11() > baseTx.m11());

    QTest::keyClick(window.get(), Qt::Key_Minus, Qt::ControlModifier);
    QTest::keyClick(window.get(), Qt::Key_Minus, Qt::ControlModifier);
    QVERIFY(view->transform().m11() < baseTx.m11());

    QTest::keyClick(window.get(), Qt::Key_0, Qt::ControlModifier);
    QCOMPARE(view->transform().m11(), baseTx.m11());
}

void TestMainWindowGui::testFastModeToggle()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *action = window->findChild<QAction *>("actionFastMode");
    QVERIFY(action);

    bool initial = action->isChecked();
    action->trigger();
    QVERIFY(action->isChecked() != initial);

    action->trigger();
    QCOMPARE(action->isChecked(), initial);
}

void TestMainWindowGui::testFullscreenToggle()
{
    std::unique_ptr<MainWindow> window(createMW());

    bool wasFull = window->isFullScreen();
    QTest::keyClick(window.get(), Qt::Key_F11);
    // Fullscreen state may not apply in headless CI, so verify either state changed
    // or at least no crash occurred.
    bool isFull = window->isFullScreen();
    if (isFull != wasFull) {
        // State changed as expected — toggle back
        QTest::keyClick(window.get(), Qt::Key_F11);
        QCOMPARE(window->isFullScreen(), wasFull);
    }
    // If state didn't change, we're in headless mode — no-crash is sufficient.
}

void TestMainWindowGui::testLabelsUnderIconsToggle()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *action = window->findChild<QAction *>("actionLabelsUnderIcons");
    QVERIFY(action);

    bool initial = action->isChecked();
    action->trigger();
    QVERIFY(action->isChecked() != initial);

    action->trigger();
    QCOMPARE(action->isChecked(), initial);
}

// ===========================================================================
// Element manipulation via keyboard
// ===========================================================================

void TestMainWindowGui::testDeleteViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);

    clickElement(view, andGate);
    QTest::keyClick(window.get(), Qt::Key_Delete);

    QCOMPARE(static_cast<int>(scene->elements().size()), 0);
}

void TestMainWindowGui::testCopyPasteViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);

    clickElement(view, andGate);
    QTest::keyClick(window.get(), Qt::Key_C, Qt::ControlModifier);
    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);

    QCOMPARE(static_cast<int>(scene->elements().size()), 2);
}

void TestMainWindowGui::testCutPasteViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);

    clickElement(view, andGate);
    QTest::keyClick(window.get(), Qt::Key_X, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), 0);

    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);
}

void TestMainWindowGui::testUndoRedoViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);

    clickElement(view, andGate);
    QTest::keyClick(window.get(), Qt::Key_Delete);
    QCOMPARE(static_cast<int>(scene->elements().size()), 0);

    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);

    QTest::keyClick(window.get(), Qt::Key_Y, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), 0);
}

void TestMainWindowGui::testSelectAllViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    scene->addItem(new InputSwitch());
    scene->addItem(new And());
    scene->addItem(new Led());

    QTest::keyClick(window.get(), Qt::Key_A, Qt::ControlModifier);

    QCOMPARE(scene->selectedElements().size(), 3);
}

void TestMainWindowGui::testRotateRightViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    int id = andGate->id();

    clickElement(view, andGate);
    QTest::keyClick(window.get(), Qt::Key_R, Qt::ControlModifier);

    auto *rotated = dynamic_cast<GraphicElement *>(scene->itemById(id));
    QVERIFY(rotated);
    QCOMPARE(rotated->rotation(), 90.0);
}

void TestMainWindowGui::testRotateLeftViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    int id = andGate->id();

    clickElement(view, andGate);
    QTest::keyClick(window.get(), Qt::Key_R, Qt::ControlModifier | Qt::ShiftModifier);

    auto *rotated = dynamic_cast<GraphicElement *>(scene->itemById(id));
    QVERIFY(rotated);
    QCOMPARE(rotated->rotation(), -90.0);
}

void TestMainWindowGui::testFlipHorizontallyViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    int id = andGate->id();
    double rotBefore = andGate->rotation();

    clickElement(view, andGate);
    QTest::keyClick(window.get(), Qt::Key_H, Qt::ControlModifier);

    auto *flipped = dynamic_cast<GraphicElement *>(scene->itemById(id));
    QVERIFY(flipped);
    // Flip is recorded in the undo stack — verify undo restores original state
    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);
    auto *unflipped = dynamic_cast<GraphicElement *>(scene->itemById(id));
    QVERIFY(unflipped);
    QCOMPARE(unflipped->rotation(), rotBefore);
}

void TestMainWindowGui::testMuteViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *action = window->findChild<QAction *>("actionMute");
    QVERIFY(action);

    bool initial = action->isChecked();

    QTest::keyClick(window.get(), Qt::Key_M, Qt::ControlModifier);
    QVERIFY(action->isChecked() != initial);

    QTest::keyClick(window.get(), Qt::Key_M, Qt::ControlModifier);
    QCOMPARE(action->isChecked(), initial);
}

void TestMainWindowGui::testPropertyCycleSecondary()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);

    clickElement(view, andGate);

    QTest::keyClick(window.get(), Qt::Key_BraceRight);
    QTest::keyClick(window.get(), Qt::Key_BraceLeft);
    // Effect is element-type dependent; for AND gate verify element still valid
    QVERIFY(andGate->isSelected());
}

// ===========================================================================
// Element manipulation via mouse
// ===========================================================================

void TestMainWindowGui::testMouseClickSelectsElement()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);

    clickElement(view, andGate);

    QCOMPARE(scene->selectedElements().size(), 1);
    QVERIFY(andGate->isSelected());
}

void TestMainWindowGui::testMouseCtrlClickMultiSelect()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *sw = new InputSwitch();
    auto *andGate = new And();
    sw->setPos(50, 100);
    andGate->setPos(200, 100);
    scene->addItem(sw);
    scene->addItem(andGate);

    clickElement(view, sw);
    QCOMPARE(scene->selectedElements().size(), 1);

    clickElement(view, andGate, Qt::ControlModifier);
    QCOMPARE(scene->selectedElements().size(), 2);
}

void TestMainWindowGui::testMouseDragMovesElement()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    int id = andGate->id();
    QPointF origPos = andGate->pos();

    clickElement(view, andGate);
    dragElement(view, andGate->scenePos(), andGate->scenePos() + QPointF(50, 0));

    auto *moved = dynamic_cast<GraphicElement *>(scene->itemById(id));
    QVERIFY(moved);
    QVERIFY(moved->pos().x() > origPos.x());
}

void TestMainWindowGui::testMouseWheelZoom()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *view = window->currentTab()->view();

    view->resetZoom();
    QTransform baseTx = view->transform();

    wheelZoom(view, 120);

    QVERIFY(view->transform().m11() > baseTx.m11());
}

void TestMainWindowGui::testMouseClickEmptyDeselects()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);

    clickElement(view, andGate);
    QCOMPARE(scene->selectedElements().size(), 1);

    clickScene(view, QPointF(-500, -500));
    QCOMPARE(scene->selectedElements().size(), 0);
}

void TestMainWindowGui::testMousePanWithSpace()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *view = window->currentTab()->view();

    QTest::keyPress(view, Qt::Key_Space);

    QPoint center = view->viewport()->rect().center();
    QPoint end = center + QPoint(100, 50);

    QTest::mousePress(view->viewport(), Qt::LeftButton, Qt::NoModifier, center);
    for (int i = 1; i <= 5; ++i) {
        QPoint pos = center + (end - center) * i / 5;
        QMouseEvent move(QEvent::MouseMove, pos, view->viewport()->mapToGlobal(pos),
                         Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
        QApplication::sendEvent(view->viewport(), &move);
    }
    QTest::mouseRelease(view->viewport(), Qt::LeftButton, Qt::NoModifier, end);

    QTest::keyRelease(view, Qt::Key_Space);

    // Verify the viewport actually scrolled (platform-dependent magnitude, but direction is reliable)
    QPoint scrollAfter(view->horizontalScrollBar()->value(), view->verticalScrollBar()->value());
    // Pan moved right and down, so scroll values should differ (or at least no crash)
    Q_UNUSED(scrollAfter)
}

// ===========================================================================
// Export operations
// ===========================================================================

void TestMainWindowGui::testExportArduino()
{
    const QString exportPath = m_fixtureDir + "/direct_export.ino";
    std::unique_ptr<MainWindow> window(createMW());

    ScopedFileDialogStub guard;
    guard.stub.openResult = m_fixtureDir + "/test_circuit.panda";
    QTest::keyClick(window.get(), Qt::Key_O, Qt::ControlModifier);

    guard.stub.saveResult = {exportPath, "Arduino file (*.ino)"};
    QTest::keyClick(window.get(), Qt::Key_I, Qt::ControlModifier | Qt::AltModifier);

    QVERIFY(QFile::exists(exportPath));
    QVERIFY(QFileInfo(exportPath).size() > 0);
    QFile::remove(exportPath);
}

void TestMainWindowGui::testExportSystemVerilog()
{
    const QString exportPath = m_fixtureDir + "/direct_export.sv";
    std::unique_ptr<MainWindow> window(createMW());

    ScopedFileDialogStub guard;
    guard.stub.openResult = m_fixtureDir + "/test_circuit.panda";
    QTest::keyClick(window.get(), Qt::Key_O, Qt::ControlModifier);

    guard.stub.saveResult = {exportPath, "SystemVerilog file (*.sv)"};
    QTest::keyClick(window.get(), Qt::Key_O, Qt::ControlModifier | Qt::AltModifier);

    QVERIFY(QFile::exists(exportPath));
    QVERIFY(QFileInfo(exportPath).size() > 0);
    QFile::remove(exportPath);
}

// ===========================================================================
// Remaining MainWindow operations
// ===========================================================================

void TestMainWindowGui::testExitViaKeyboard()
{
    // Verify Ctrl+Q triggers close. We auto-dismiss any save prompt.
    // Use unique_ptr to avoid leaks if any assertion fails.
    std::unique_ptr<MainWindow> window(createMW());
    window->setAttribute(Qt::WA_DeleteOnClose, false);
    QVERIFY(window->isVisible());

    // closeEvent shows "Are you sure?" even with no unsaved changes.
    // Accept any QMessageBox that appears during the close sequence.
    auto dismissDialogs = [] {
        if (auto *w = QApplication::activeModalWidget()) {
            if (auto *msgBox = qobject_cast<QMessageBox *>(w)) {
                if (auto *btn = msgBox->button(QMessageBox::Yes)) { btn->click(); return; }
                if (auto *btn = msgBox->button(QMessageBox::No)) { btn->click(); return; }
                if (auto *btn = msgBox->button(QMessageBox::NoToAll)) { btn->click(); return; }
                msgBox->accept();
            }
        }
    };
    // Schedule multiple dismiss attempts to handle chained dialogs
    QTimer::singleShot(0, dismissDialogs);
    QTimer::singleShot(100, dismissDialogs);
    QTimer::singleShot(200, dismissDialogs);

    QTest::keyClick(window.get(), Qt::Key_Q, Qt::ControlModifier);

    QVERIFY(!window->isVisible());
}

void TestMainWindowGui::testOpenRecentFile()
{
    const QString savePath = m_fixtureDir + "/recent_test.panda";

    {
        std::unique_ptr<MainWindow> window(createMW());
        window->currentTab()->scene()->addItem(new InputSwitch());

        ScopedFileDialogStub guard;
        guard.stub.saveResult = {savePath, "Panda files (*.panda)"};
        QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);
    }

    {
        std::unique_ptr<MainWindow> window(createMW());
        auto *menuRecentFiles = window->findChild<QMenu *>("menuRecentFiles");
        QVERIFY2(menuRecentFiles, "Recent files menu not found");

        // Recent files list may be empty if QSettings doesn't persist between test
        // MainWindow instances. Verify the menu exists and is accessible regardless.
        QVERIFY(menuRecentFiles->menuAction()->isEnabled());
        if (!menuRecentFiles->actions().isEmpty()) {
            auto *recentAction = menuRecentFiles->actions().first();
            QVERIFY(recentAction);
            recentAction->trigger();
            QVERIFY(!window->currentTab()->scene()->elements().isEmpty());
        }
    }

    QFile::remove(savePath);
}

void TestMainWindowGui::testBackgroundSimulation()
{
    std::unique_ptr<MainWindow> window(createMW());

    auto *action = window->findChild<QAction *>("actionBackground_Simulation");
    QVERIFY2(action, "actionBackground_Simulation not found");

    bool initial = action->isChecked();
    action->trigger();
    QVERIFY(action->isChecked() != initial);

    action->trigger();
    QCOMPARE(action->isChecked(), initial);
}

void TestMainWindowGui::testOpenWaveformEditor()
{
    std::unique_ptr<MainWindow> window(createMW());

    auto *sw = new InputSwitch();
    auto *led = new Led();
    window->currentTab()->scene()->addItem(sw);
    window->currentTab()->scene()->addItem(led);
    CircuitBuilder builder(window->currentTab()->scene());
    builder.connect(sw, 0, led, 0);

    QTimer::singleShot(50, [&window] {
        for (auto *w : QApplication::topLevelWidgets()) {
            if (auto *mw = qobject_cast<QMainWindow *>(w)) {
                if (mw != window.get()) {
                    mw->setAttribute(Qt::WA_DeleteOnClose, false);
                    mw->close();
                    mw->deleteLater();
                }
            }
        }
    });

    QTest::keyClick(window.get(), Qt::Key_W, Qt::ControlModifier);

    QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
    QCoreApplication::processEvents();
}

void TestMainWindowGui::testLanguageChange()
{
    std::unique_ptr<MainWindow> window(createMW());
    window->loadTranslation("en");
    QString titleEn = window->windowTitle();

    window->loadTranslation("pt_BR");
    // Title may or may not change depending on whether translations are bundled in test binary.
    // At minimum verify no crash during switching.

    window->loadTranslation("en");
    QCOMPARE(window->windowTitle(), titleEn);
}

// ===========================================================================
// Help dialogs
// ===========================================================================

void TestMainWindowGui::testAboutDialog()
{
    std::unique_ptr<MainWindow> window(createMW());
    autoCloseNextMessageBox();

    auto *action = window->findChild<QAction *>("actionAbout");
    QVERIFY(action);
    action->trigger();
}

void TestMainWindowGui::testAboutQtDialog()
{
    std::unique_ptr<MainWindow> window(createMW());
    autoCloseNextMessageBox();

    auto *action = window->findChild<QAction *>("actionAboutQt");
    QVERIFY(action);
    action->trigger();
}

void TestMainWindowGui::testShortcutsDialog()
{
    std::unique_ptr<MainWindow> window(createMW());
    autoCloseNextMessageBox();

    auto *action = window->findChild<QAction *>("actionShortcutsAndTips");
    QVERIFY2(action, "actionShortcutsAndTips not found");
    action->trigger();
}

// ===========================================================================
// ElementEditor operations
// ===========================================================================

void TestMainWindowGui::testChangeTriggerKey()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *button = new InputButton();
    workspace.scene()->addItem(button);
    button->setSelected(true);
    QCoreApplication::processEvents();

    auto *triggerEdit = editor.findChild<QLineEdit *>("lineEditTrigger");
    QVERIFY2(triggerEdit, "lineEditTrigger not found in ElementEditor");
    triggerEdit->setFocus();
    QTest::keyClick(triggerEdit, Qt::Key_A);
    QCoreApplication::processEvents();

    QCOMPARE(button->trigger(), QKeySequence(Qt::Key_A));
}

void TestMainWindowGui::testChangeSkin()
{
    ScopedFileDialogStub guard;
    guard.stub.openResult = QString(); // cancelled

    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *andGate = new And();
    workspace.scene()->addItem(andGate);
    andGate->setSelected(true);
    QCoreApplication::processEvents();

    auto *skinButton = editor.findChild<QPushButton *>("pushButtonChangeAppearance");
    QVERIFY2(skinButton, "pushButtonChangeAppearance not found");
    QTest::mouseClick(skinButton, Qt::LeftButton);

    QCOMPARE(guard.stub.openCallCount, 1);
    QVERIFY(guard.stub.lastOpenCall.filter.contains("Images"));
}

void TestMainWindowGui::testDefaultSkin()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *andGate = new And();
    workspace.scene()->addItem(andGate);
    andGate->setSelected(true);
    QCoreApplication::processEvents();

    auto *defaultButton = editor.findChild<QPushButton *>("pushButtonDefaultAppearance");
    QVERIFY2(defaultButton, "pushButtonDefaultAppearance not found");

    QTest::mouseClick(defaultButton, Qt::LeftButton);

    // Verify element still valid and skin was reverted
    QVERIFY(andGate->isSelected());
}

void TestMainWindowGui::testChangeDelay()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *node = new Node();
    workspace.scene()->addItem(node);
    node->setSelected(true);
    QCoreApplication::processEvents();

    auto *slider = editor.findChild<QSlider *>();
    QVERIFY2(slider, "Delay slider not found");

    int target = slider->minimum() + 1;
    slider->setValue(target);
    QCoreApplication::processEvents();
    QCOMPARE(slider->value(), target);
}

// ===========================================================================
// Embedded IC operations via keyboard
// ===========================================================================

/// Adds an embedded IC to a scene for testing.
static IC *placeEmbeddedIC(Scene *scene, const QString &fixtureDir,
                           const QString &blobName, const QPointF &pos = {100, 100})
{
    const QString path = fixtureDir + "/test_circuit.panda";
    QByteArray blob = ICTestHelpers::readFile(path);
    auto *reg = scene->icRegistry();
    auto *ic = new IC();
    ICTestHelpers::embedIC(ic, blob, blobName, fixtureDir, reg);
    ic->setPos(pos);
    scene->addItem(ic);
    return ic;
}

static int countEmbeddedICsInScene(Scene *scene, const QString &blobName = {})
{
    int count = 0;
    for (auto *elm : scene->elements()) {
        if (elm->isEmbedded()) {
            if (blobName.isEmpty() || elm->blobName() == blobName) {
                ++count;
            }
        }
    }
    return count;
}

void TestMainWindowGui::testEmbeddedICCopyPaste()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "copy_test");
    QCOMPARE(countEmbeddedICsInScene(scene), 1);

    // Click to select, Ctrl+C, Ctrl+V
    clickElement(view, ic);
    QTest::keyClick(window.get(), Qt::Key_C, Qt::ControlModifier);
    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);

    QCOMPARE(countEmbeddedICsInScene(scene, "copy_test"), 2);

    // Both should be functional
    for (auto *elm : scene->elements()) {
        if (elm->isEmbedded()) {
            QCOMPARE(elm->blobName(), QString("copy_test"));
            QVERIFY(elm->inputSize() > 0);
        }
    }
}

void TestMainWindowGui::testEmbeddedICCutPaste()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "cut_test");
    QCOMPARE(countEmbeddedICsInScene(scene), 1);

    // Click, Ctrl+X, verify gone, Ctrl+V, verify back
    clickElement(view, ic);
    QTest::keyClick(window.get(), Qt::Key_X, Qt::ControlModifier);
    QCOMPARE(countEmbeddedICsInScene(scene), 0);

    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);
    QCOMPARE(countEmbeddedICsInScene(scene, "cut_test"), 1);
}

void TestMainWindowGui::testEmbeddedICDeleteUndo()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "del_test");
    auto *sw = new InputSwitch();
    auto *led = new Led();
    sw->setPos(50, 100);
    led->setPos(250, 100);
    scene->addItem(sw);
    scene->addItem(led);

    CircuitBuilder builder(scene);
    builder.connect(sw, 0, ic, 0);
    builder.connect(ic, 0, led, 0);

    int totalConns = TestUtils::countConnections(scene);
    QCOMPARE(totalConns, 2);

    // Click on IC center (scenePos + half bounding rect) to select, then Del
    QPointF icCenter = ic->scenePos() + ic->boundingRect().center();
    clickScene(view, icCenter);
    QCOMPARE(scene->selectedElements().size(), 1);
    QTest::keyClick(window.get(), Qt::Key_Delete);

    QCOMPARE(countEmbeddedICsInScene(scene), 0);
    QCOMPARE(TestUtils::countConnections(scene), 0);

    // Ctrl+Z to undo
    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);

    QCOMPARE(countEmbeddedICsInScene(scene, "del_test"), 1);
    QCOMPARE(TestUtils::countConnections(scene), totalConns);
}

void TestMainWindowGui::testEmbeddedICSelectAllDeleteUndo()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    placeEmbeddedIC(scene, m_fixtureDir, "selall", {100, 100});
    scene->addItem(new And());
    scene->addItem(new Led());

    int origCount = static_cast<int>(scene->elements().size());
    QCOMPARE(origCount, 3);

    // Ctrl+A, Del
    QTest::keyClick(window.get(), Qt::Key_A, Qt::ControlModifier);
    QTest::keyClick(window.get(), Qt::Key_Delete);
    QCOMPARE(static_cast<int>(scene->elements().size()), 0);

    // Ctrl+Z
    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), origCount);
    QCOMPARE(countEmbeddedICsInScene(scene), 1);
}

void TestMainWindowGui::testEmbeddedICRotatePreservesState()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "rot_test");
    int icId = ic->id();

    // Click, Ctrl+R
    clickElement(view, ic);
    QTest::keyClick(window.get(), Qt::Key_R, Qt::ControlModifier);

    auto *rotated = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(rotated);
    QCOMPARE(rotated->rotation(), 90.0);
    QVERIFY(rotated->isEmbedded());
    QCOMPARE(rotated->blobName(), QString("rot_test"));

    // Ctrl+Z, verify rotation reverted but still embedded
    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);
    auto *undone = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(undone);
    QCOMPARE(undone->rotation(), 0.0);
    QVERIFY(undone->isEmbedded());
}

void TestMainWindowGui::testEmbeddedICSimulationAfterDeleteUndo()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    auto *swA = new InputSwitch();
    auto *swB = new InputSwitch();
    auto *led = new Led();
    swA->setPos(50, 80);
    swB->setPos(50, 120);
    led->setPos(300, 100);
    scene->addItem(swA);
    scene->addItem(swB);
    scene->addItem(led);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "sim_test", {180, 100});

    CircuitBuilder builder(scene);
    builder.connect(swA, 0, ic, 0);
    if (ic->inputSize() > 1) {
        builder.connect(swB, 0, ic, 1);
    }
    builder.connect(ic, 0, led, 0);

    auto *sim = builder.initSimulation();
    swA->setOn(false);
    swB->setOn(false);
    sim->update();
    bool outputOff = TestUtils::getInputStatus(led);

    // Also test with inputs ON to verify IC is actually processing signals
    swA->setOn(true);
    sim->update();
    bool outputOn = TestUtils::getInputStatus(led);

    // Delete IC, undo, re-simulate
    clickElement(view, ic);
    QTest::keyClick(window.get(), Qt::Key_Delete);
    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);

    sim = builder.initSimulation();
    swA->setOn(false);
    swB->setOn(false);
    sim->update();
    QCOMPARE(TestUtils::getInputStatus(led), outputOff);

    // Verify with inputs ON — IC must be functionally processing signals
    swA->setOn(true);
    sim->update();
    QCOMPARE(TestUtils::getInputStatus(led), outputOn);
}

void TestMainWindowGui::testEmbeddedICMultipleTypesDeleteOne()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    auto *alpha1 = placeEmbeddedIC(scene, m_fixtureDir, "type_a", {100, 100});
    auto *alpha2 = placeEmbeddedIC(scene, m_fixtureDir, "type_a", {200, 100});
    placeEmbeddedIC(scene, m_fixtureDir, "type_b", {100, 200});

    QCOMPARE(countEmbeddedICsInScene(scene, "type_a"), 2);
    QCOMPARE(countEmbeddedICsInScene(scene, "type_b"), 1);

    // Click alpha1, Ctrl+click alpha2, Del
    clickElement(view, alpha1);
    clickElement(view, alpha2, Qt::ControlModifier);
    QTest::keyClick(window.get(), Qt::Key_Delete);

    QCOMPARE(countEmbeddedICsInScene(scene, "type_a"), 0);
    QCOMPARE(countEmbeddedICsInScene(scene, "type_b"), 1);

    // Ctrl+Z restores
    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);
    QCOMPARE(countEmbeddedICsInScene(scene, "type_a"), 2);
    QCOMPARE(countEmbeddedICsInScene(scene, "type_b"), 1);
}

// ===========================================================================
// Embedded IC edge cases
// ===========================================================================

void TestMainWindowGui::testEmbeddedICCrossTabCopyPaste()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());

    // Tab 1: place an embedded IC
    auto *scene1 = window->currentTab()->scene();
    auto *view1 = window->currentTab()->view();
    scene1->setContextDir(m_fixtureDir);
    auto *ic = placeEmbeddedIC(scene1, m_fixtureDir, "cross_tab");
    QVERIFY(scene1->icRegistry()->hasBlob("cross_tab"));

    // Copy in tab 1
    clickElement(view1, ic);
    QTest::keyClick(window.get(), Qt::Key_C, Qt::ControlModifier);

    // Create tab 2
    QTest::keyClick(window.get(), Qt::Key_N, Qt::ControlModifier);
    QCOMPARE(tabs->count(), 2);
    auto *scene2 = window->currentTab()->scene();

    // Paste in tab 2
    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);

    // Verify blob was imported into tab 2's registry
    QVERIFY(scene2->icRegistry()->hasBlob("cross_tab"));
    QCOMPARE(countEmbeddedICsInScene(scene2, "cross_tab"), 1);
}

void TestMainWindowGui::testMakeSelfContainedWithFileICs()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    // Add a file-backed IC
    auto *ic = new IC();
    ic->loadFile("test_circuit.panda", m_fixtureDir);
    ic->setPos(100, 100);
    scene->addItem(ic);
    QVERIFY(!ic->isEmbedded());

    // Save first so contextDir is set
    ScopedFileDialogStub guard;
    const QString savePath = m_fixtureDir + "/self_contained_test.panda";
    guard.stub.saveResult = {savePath, "Panda files (*.panda)"};
    QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);

    // Trigger makeSelfContained
    autoCloseNextMessageBox();
    auto *action = window->findChild<QAction *>("actionMakeSelfContained");
    QVERIFY(action);
    action->trigger();

    // All file-backed ICs should now be embedded
    bool allEmbedded = true;
    for (auto *elm : scene->elements()) {
        if (elm->elementType() == ElementType::IC && !elm->isEmbedded()) {
            allEmbedded = false;
            break;
        }
    }
    QVERIFY2(allEmbedded, "Not all file-backed ICs were embedded by makeSelfContained");

    // Undo should restore file-backed state
    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);
    bool anyFileBacked = false;
    for (auto *elm : scene->elements()) {
        if (elm->elementType() == ElementType::IC && !elm->isEmbedded()) {
            anyFileBacked = true;
            break;
        }
    }
    QVERIFY2(anyFileBacked, "Undo after makeSelfContained did not restore file-backed ICs");

    QFile::remove(savePath);
}

void TestMainWindowGui::testMakeSelfContainedMixedScene()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    // Add one embedded IC
    placeEmbeddedIC(scene, m_fixtureDir, "already_embedded", {100, 100});

    // Add one file-backed IC
    auto *fileIC = new IC();
    fileIC->loadFile("test_circuit.panda", m_fixtureDir);
    fileIC->setPos(200, 100);
    scene->addItem(fileIC);

    QCOMPARE(countEmbeddedICsInScene(scene), 1);

    // Save first
    ScopedFileDialogStub guard;
    const QString savePath = m_fixtureDir + "/mixed_test.panda";
    guard.stub.saveResult = {savePath, "Panda files (*.panda)"};
    QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);

    // makeSelfContained should only convert the file-backed IC
    autoCloseNextMessageBox();
    auto *action = window->findChild<QAction *>("actionMakeSelfContained");
    action->trigger();

    // Now all ICs should be embedded
    int embeddedCount = countEmbeddedICsInScene(scene);
    QCOMPARE(embeddedCount, 2);

    // Verify the originally-embedded IC kept its identity
    QCOMPARE(countEmbeddedICsInScene(scene, "already_embedded"), 1);

    QFile::remove(savePath);
}

void TestMainWindowGui::testMakeSelfContainedLabelsPreserved()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    auto *ic1 = new IC();
    ic1->loadFile("test_circuit.panda", m_fixtureDir);
    ic1->setPos(100, 100);
    ic1->setLabel("LabelA");
    scene->addItem(ic1);

    auto *ic2 = new IC();
    ic2->loadFile("test_circuit.panda", m_fixtureDir);
    ic2->setPos(200, 100);
    ic2->setLabel("LabelB");
    scene->addItem(ic2);

    // Save
    ScopedFileDialogStub guard;
    const QString savePath = m_fixtureDir + "/labels_test.panda";
    guard.stub.saveResult = {savePath, "Panda files (*.panda)"};
    QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);

    autoCloseNextMessageBox();
    auto *action = window->findChild<QAction *>("actionMakeSelfContained");
    action->trigger();

    // Verify ICs are actually embedded AND labels preserved
    QStringList labels;
    int embeddedCount = 0;
    for (auto *elm : scene->elements()) {
        if (elm->elementType() == ElementType::IC) {
            QVERIFY2(elm->isEmbedded(), "IC should be embedded after makeSelfContained");
            labels.append(elm->label());
            ++embeddedCount;
        }
    }
    QCOMPARE(embeddedCount, 2);
    QVERIFY(labels.contains("LabelA"));
    QVERIFY(labels.contains("LabelB"));

    QFile::remove(savePath);
}

void TestMainWindowGui::testEmbedExtractViaContextMenuCallback()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    // Add a file-backed IC
    auto *ic = new IC();
    ic->loadFile("test_circuit.panda", m_fixtureDir);
    ic->setPos(100, 100);
    scene->addItem(ic);
    QVERIFY(!ic->isEmbedded());
    int icId = ic->id();

    // Select the IC via mouse click (required for embedSelectedIC)
    clickElement(view, ic);
    QVERIFY(ic->isSelected());

    // Trigger embed via the ElementEditor signal (same path as context menu
    // "Embed sub-circuit" → ElementEditor::embedSubcircuitRequested → MainWindow::embedSelectedIC)
    auto *editor = window->findChild<ElementEditor *>();
    QVERIFY2(editor, "ElementEditor not found");
    emit editor->embedSubcircuitRequested();

    // Verify embedded
    auto *embeddedIC = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(embeddedIC);
    QVERIFY2(embeddedIC->isEmbedded(), "IC should be embedded after embed signal");
    QVERIFY(!embeddedIC->blobName().isEmpty());
    QString blobName = embeddedIC->blobName();
    QVERIFY(scene->icRegistry()->hasBlob(blobName));

    // Select the now-embedded IC for extraction
    clickElement(view, embeddedIC);
    QVERIFY(embeddedIC->isSelected());

    // Trigger extract via the ElementEditor signal (same path as context menu
    // "Extract to file" → ElementEditor::extractToFileRequested → MainWindow::extractSelectedIC)
    ScopedFileDialogStub guard;
    const QString extractPath = m_fixtureDir + "/extracted_test.panda";
    guard.stub.saveResult = {extractPath, "Panda files (*.panda)"};
    emit editor->extractToFileRequested();
    auto *extractedIC = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(extractedIC);
    QVERIFY2(!extractedIC->isEmbedded(), "IC should be file-backed after extract signal");
    QVERIFY(QFile::exists(extractPath));

    // Undo extract → should re-embed
    scene->undoStack()->undo();
    auto *undoneIC = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(undoneIC);
    QVERIFY2(undoneIC->isEmbedded(), "IC should be re-embedded after undo of extract");
    QVERIFY(scene->icRegistry()->hasBlob(blobName));

    // Undo embed → should restore file-backed
    scene->undoStack()->undo();
    auto *originalIC = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(originalIC);
    QVERIFY2(!originalIC->isEmbedded(), "IC should be file-backed after undo of embed");

    // Redo embed → should re-embed
    scene->undoStack()->redo();
    auto *redoneIC = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(redoneIC);
    QVERIFY2(redoneIC->isEmbedded(), "IC should be re-embedded after redo of embed");

    QFile::remove(extractPath);
}

void TestMainWindowGui::testDeleteLastEmbeddedInstanceUndo()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "last_one");
    QCOMPARE(countEmbeddedICsInScene(scene, "last_one"), 1);

    // Blob should be in registry
    QVERIFY(scene->icRegistry()->hasBlob("last_one"));

    // Delete the last (and only) instance
    clickElement(view, ic);
    QTest::keyClick(window.get(), Qt::Key_Delete);
    QCOMPARE(countEmbeddedICsInScene(scene, "last_one"), 0);

    // Blob should still exist as orphan in registry (not removed by delete)
    QVERIFY(scene->icRegistry()->hasBlob("last_one"));

    // Undo should restore a functional IC
    QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);
    QCOMPARE(countEmbeddedICsInScene(scene, "last_one"), 1);

    // Verify restored IC is functional
    for (auto *elm : scene->elements()) {
        if (elm->isEmbedded() && elm->blobName() == "last_one") {
            QVERIFY(elm->inputSize() > 0);
            QVERIFY(elm->outputSize() > 0);
        }
    }
}

void TestMainWindowGui::testReEmbedAfterExtract()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    // Place an embedded IC
    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "reembed_test");
    int icId = ic->id();
    QVERIFY(ic->isEmbedded());

    // Extract via GUI signal
    clickElement(view, ic);
    ScopedFileDialogStub guard;
    const QString extractPath = m_fixtureDir + "/reembed_extracted.panda";
    guard.stub.saveResult = {extractPath, "Panda files (*.panda)"};
    auto *editor = window->findChild<ElementEditor *>();
    QVERIFY(editor);
    emit editor->extractToFileRequested();

    auto *extractedIC = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(extractedIC);
    QVERIFY2(!extractedIC->isEmbedded(), "IC should be file-backed after extract");
    QVERIFY(QFile::exists(extractPath));

    // Re-embed via GUI signal (select the now file-backed IC and embed it)
    clickElement(view, extractedIC);
    emit editor->embedSubcircuitRequested();

    auto *reembeddedIC = dynamic_cast<IC *>(scene->itemById(icId));
    QVERIFY(reembeddedIC);
    QVERIFY2(reembeddedIC->isEmbedded(), "IC should be re-embedded after embed signal");
    QVERIFY(reembeddedIC->inputSize() > 0);

    QFile::remove(extractPath);
}

void TestMainWindowGui::testInlineTabDeduplication()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "dedup_test");
    QByteArray blob = scene->icRegistry()->blob("dedup_test");

    int initialTabs = tabs->count();

    // Open inline tab for the embedded IC
    window->openICInTab("dedup_test", ic->id(), blob);
    int afterFirst = tabs->count();
    QCOMPARE(afterFirst, initialTabs + 1);

    // Try to open the same blob again — should switch to existing tab, not create new
    // Switch back to the parent tab first
    tabs->setCurrentIndex(0);
    window->openICInTab("dedup_test", ic->id(), blob);
    QCOMPARE(tabs->count(), afterFirst);
}

void TestMainWindowGui::testEmbeddedICSaveReloadRoundTrip()
{
    const QString savePath = m_fixtureDir + "/save_reload_test.panda";

    // Create a MainWindow, place an embedded IC, save the project
    {
        std::unique_ptr<MainWindow> window(createMW());
        auto *scene = window->currentTab()->scene();
        scene->setContextDir(m_fixtureDir);
        placeEmbeddedIC(scene, m_fixtureDir, "save_reload");

        ScopedFileDialogStub guard;
        guard.stub.saveResult = {savePath, "Panda files (*.panda)"};
        QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);
    }

    // Reload in a new MainWindow
    {
        std::unique_ptr<MainWindow> window2(createMW());
        window2->loadPandaFile(savePath);
        auto *scene2 = window2->currentTab()->scene();

        // Verify embedded IC survived the round-trip
        QVERIFY(scene2->icRegistry()->hasBlob("save_reload"));
        int found = 0;
        for (auto *elm : scene2->elements()) {
            if (elm->isEmbedded() && elm->blobName() == "save_reload") {
                ++found;
                QVERIFY(elm->inputSize() > 0);
                QVERIFY(elm->outputSize() > 0);
            }
        }
        QCOMPARE(found, 1);
    }

    QFile::remove(savePath);
}

void TestMainWindowGui::testEmbeddedICCopyPastePreservesState()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();
    scene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "clone_test");
    QCOMPARE(countEmbeddedICsInScene(scene, "clone_test"), 1);

    // Select the IC and copy/paste via keyboard to simulate clone
    clickElement(view, ic);
    QTest::keyClick(window.get(), Qt::Key_C, Qt::ControlModifier);
    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);

    // Should have 2 instances now
    QCOMPARE(countEmbeddedICsInScene(scene, "clone_test"), 2);

    // Both should be functional
    for (auto *elm : scene->elements()) {
        if (elm->isEmbedded() && elm->blobName() == "clone_test") {
            QVERIFY(elm->inputSize() > 0);
        }
    }
}

// ===========================================================================
// Inline IC tab operations
// ===========================================================================

void TestMainWindowGui::testInlineICDropSaveCloseReopen()
{
    // Reproduces the user-reported bug:
    // 1. Open a project with an embedded IC
    // 2. Double-click the IC → opens inline tab (via connectTab/openICInTab)
    // 3. Drop a file-based IC into the inline tab
    // 4. Save (Ctrl+S) → file-based IC should be converted to embedded
    // 5. Close the inline tab
    // 6. Re-open the same IC → should NOT give "Invalid file format"
    //
    // The bug was caused by stale icOpenRequested connections accumulating
    // because disconnectTab() didn't disconnect them. This test exercises
    // the full MainWindow tab lifecycle that unit-level WorkSpace tests miss.

    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    // Step 1: Place an embedded IC in the root scene.
    // Use a second fixture as the "file-based IC to drop later".
    auto *embeddedIC = placeEmbeddedIC(scene, m_fixtureDir, "parent_ic");
    const int embeddedICId = embeddedIC->id();
    QByteArray blob = scene->icRegistry()->blob("parent_ic");
    QVERIFY(!blob.isEmpty());

    // Create a second fixture file to act as a file-based IC we will drop.
    const QString dropFile = m_fixtureDir + "/drop_target.panda";
    MainWindowGuiHelpers::createMWFixture(dropFile);

    const int rootTabIndex = tabs->currentIndex();
    auto *rootTab = window->currentTab();

    // Step 2: Open the embedded IC in an inline tab.
    window->openICInTab("parent_ic", embeddedICId, blob);
    QCOMPARE(tabs->count(), 2);
    auto *inlineTab = window->currentTab();
    QVERIFY(inlineTab->isInlineIC());

    // Step 3: Drop a file-based IC into the inline tab's scene.
    auto *droppedIC = new IC();
    droppedIC->loadFile(dropFile, m_fixtureDir);
    droppedIC->setPos(200, 200);
    inlineTab->scene()->addItem(droppedIC);
    QVERIFY2(!droppedIC->isEmbedded(), "Dropped IC should be file-backed before save");
    QVERIFY(droppedIC->inputSize() > 0);

    // Step 4: Save via Ctrl+S — should NOT open a file dialog.
    ScopedFileDialogStub guard;
    QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);
    QCOMPARE(guard.stub.saveCallCount, 0);
    QCOMPARE(guard.stub.openCallCount, 0);

    // The dropped IC should now be embedded.
    QVERIFY2(droppedIC->isEmbedded(), "IC should be embedded after inline save");
    QCOMPARE(droppedIC->blobName(), QString("drop_target"));

    // Inline tab title must stay "[parent_ic]", not change to the parent filename.
    QCOMPARE(tabs->tabText(tabs->indexOf(inlineTab)), QString("[parent_ic]"));

    // The parent tab should be marked dirty (asterisk) because onChildICBlobSaved
    // pushed an UpdateBlobCommand to the root scene's undo stack.
    const QString rootTitle = tabs->tabText(rootTabIndex);
    QVERIFY2(rootTitle.endsWith("*"), qPrintable("Root tab should be dirty: " + rootTitle));

    // The parent's registry should have been updated.
    QByteArray updatedBlob = rootTab->scene()->icRegistry()->blob("parent_ic");
    QVERIFY2(!updatedBlob.isEmpty(), "Root registry should have updated parent_ic blob");

    // Step 5: Close the inline tab.
    const int inlineTabIndex = tabs->indexOf(inlineTab);
    tabs->setCurrentIndex(inlineTabIndex);
    // Process deferred delete events from tab close
    inlineTab->deleteLater();
    tabs->removeTab(inlineTabIndex);
    QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
    QCoreApplication::processEvents();

    // Switch back to root tab.
    tabs->setCurrentIndex(rootTabIndex);
    QCOMPARE(window->currentTab(), rootTab);

    // Step 6: Re-open the same embedded IC — should NOT throw "Invalid file format".
    updatedBlob = rootTab->scene()->icRegistry()->blob("parent_ic");
    QVERIFY(!updatedBlob.isEmpty());

    bool reopenOK = false;
    try {
        window->openICInTab("parent_ic", embeddedICId, updatedBlob);
        reopenOK = true;
    } catch (const std::exception &e) {
        QFAIL(qPrintable(QString("Reopen threw: %1").arg(e.what())));
    }
    QVERIFY(reopenOK);
    QCOMPARE(tabs->count(), 2);

    auto *reopenedTab = window->currentTab();
    QVERIFY(reopenedTab->isInlineIC());

    // The drop_target IC should be present and embedded in the reopened tab.
    bool foundDropped = false;
    for (auto *elm : reopenedTab->scene()->elements()) {
        if (elm->isEmbedded() && elm->blobName() == "drop_target") {
            foundDropped = true;
            QVERIFY(elm->inputSize() > 0);
        }
    }
    QVERIFY2(foundDropped, "Reopened inline tab should contain the embedded drop_target IC");

    QFile::remove(dropFile);
}

void TestMainWindowGui::testInlineICSaveNoFileDialog()
{
    // Verifies that Ctrl+S on an inline IC tab does NOT open a file dialog.
    // Before the fix, on_actionSave_triggered checked currentFile() which is
    // empty for inline tabs, causing it to show a Save-As dialog.

    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "no_dialog_test");
    QByteArray blob = scene->icRegistry()->blob("no_dialog_test");

    // Open inline tab
    window->openICInTab("no_dialog_test", ic->id(), blob);
    QVERIFY(window->currentTab()->isInlineIC());

    // Add an element so there's something to save
    window->currentTab()->scene()->addItem(new And());

    // Save — stub should NOT be called
    ScopedFileDialogStub guard;
    QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);

    QCOMPARE(guard.stub.saveCallCount, 0);
    QCOMPARE(guard.stub.openCallCount, 0);
}

void TestMainWindowGui::testInlineICSaveMarksRootDirty()
{
    // When an inline IC tab saves, onChildICBlobSaved pushes an
    // UpdateBlobCommand to the root scene's undo stack, which should
    // mark the root tab as dirty (not clean).

    std::unique_ptr<MainWindow> window(createMW());
    auto *rootScene = window->currentTab()->scene();
    rootScene->setContextDir(m_fixtureDir);

    auto *ic = placeEmbeddedIC(rootScene, m_fixtureDir, "dirty_test");
    QByteArray blob = rootScene->icRegistry()->blob("dirty_test");

    // Save root so its undo stack is clean
    const QString rootPath = m_fixtureDir + "/dirty_root.panda";
    window->save(rootPath);
    QVERIFY(rootScene->undoStack()->isClean());

    // Open inline tab
    window->openICInTab("dirty_test", ic->id(), blob);
    QVERIFY(window->currentTab()->isInlineIC());

    // Add an element and save inline tab
    window->currentTab()->scene()->addItem(new Led());

    ScopedFileDialogStub guard;
    QTest::keyClick(window.get(), Qt::Key_S, Qt::ControlModifier);
    QCOMPARE(guard.stub.saveCallCount, 0);

    // Root tab's undo stack should now be dirty
    QVERIFY2(!rootScene->undoStack()->isClean(),
             "Root undo stack should be dirty after inline IC child save");

    QFile::remove(rootPath);
}

// ===========================================================================
// Edge cases & missing coverage
// ===========================================================================

void TestMainWindowGui::testClearSelectionViaEscape()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    scene->addItem(new InputSwitch());
    scene->addItem(new And());
    scene->addItem(new Led());

    scene->selectAll();
    QCOMPARE(scene->selectedElements().size(), 3);

    auto *action = window->findChild<QAction *>("actionClearSelection");
    QVERIFY2(action, "actionClearSelection not found");
    action->trigger();
    QCOMPARE(scene->selectedElements().size(), 0);
}

void TestMainWindowGui::testFlipVertically()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);

    clickElement(view, andGate);
    QVERIFY(andGate->isSelected());

    // Flip via scene API and verify undo stack recorded the command
    QVERIFY(scene->undoStack());
    int stackBefore = scene->undoStack()->count();

    scene->flipVertically();

    QCOMPARE(scene->undoStack()->count(), stackBefore + 1);
}

void TestMainWindowGui::testCloseLastTab()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());
    QCOMPARE(tabs->count(), 1);

    // Closing the last tab should not crash
    tabs->tabCloseRequested(0);

    // Tab count may reach 0 — window remains valid and operational
    QVERIFY(window->isVisible());
}

void TestMainWindowGui::testEmptySceneDelete()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    // Delete with nothing selected should be a no-op
    QCOMPARE(scene->selectedElements().size(), 0);
    QTest::keyClick(window.get(), Qt::Key_Delete);
    QCOMPARE(static_cast<int>(scene->elements().size()), 0);
}

void TestMainWindowGui::testEmptySceneCopyPaste()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    // Copy with nothing selected — should be a no-op
    scene->copyAction();

    // Paste from the empty-selection copy — should be a no-op without crash
    scene->pasteAction();

    QCOMPARE(static_cast<int>(scene->elements().size()), 0);
}

void TestMainWindowGui::testMaxZoom()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *view = window->currentTab()->view();

    // Zoom in repeatedly past the limit using direct API
    for (int i = 0; i < 20; ++i) {
        view->zoomIn();
    }

    // Should cap at maximum zoom without crash
    QVERIFY(!view->canZoomIn());
    QVERIFY(view->canZoomOut());
}

void TestMainWindowGui::testMinZoom()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *view = window->currentTab()->view();

    // Zoom out repeatedly past the limit using direct API
    for (int i = 0; i < 20; ++i) {
        view->zoomOut();
    }

    // Should cap at minimum zoom without crash
    QVERIFY(view->canZoomIn());
    QVERIFY(!view->canZoomOut());
}

void TestMainWindowGui::testUndoPastStack()
{
    std::unique_ptr<MainWindow> window(createMW());

    // Undo with empty stack should be a no-op
    for (int i = 0; i < 5; ++i) {
        QTest::keyClick(window.get(), Qt::Key_Z, Qt::ControlModifier);
    }

    // No crash = pass; scene should still be functional
    QVERIFY(window->currentTab()->scene() != nullptr);
}

void TestMainWindowGui::testRedoAfterNewAction()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    // Add element, select, delete via action
    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    clickElement(view, andGate);
    scene->deleteAction();
    QCOMPARE(static_cast<int>(scene->elements().size()), 0);

    // Undo via scene's undo action
    auto *undoAction = scene->undoAction();
    QVERIFY(undoAction);
    undoAction->trigger();
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);

    // Perform a NEW delete — this should clear the redo stack
    auto *led = new Led();
    led->setPos(200, 200);
    scene->addItem(led);
    clickElement(view, led);
    scene->deleteAction();

    undoAction->trigger();
    QCOMPARE(static_cast<int>(scene->elements().size()), 2);

    // Redo should only redo the LED delete
    auto *redoAction = scene->redoAction();
    QVERIFY(redoAction);
    redoAction->trigger();
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);

    // Another redo should be a no-op
    redoAction->trigger();
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);
}

void TestMainWindowGui::testWireCreationViaMouse()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *sw = new InputSwitch();
    auto *led = new Led();
    sw->setPos(50, 100);
    led->setPos(250, 100);
    scene->addItem(sw);
    scene->addItem(led);

    // Get port scene positions
    QPointF outPortPos = sw->outputPort(0)->scenePos();
    QPointF inPortPos = led->inputPort(0)->scenePos();

    // Drag from output port to input port
    dragElement(view, outPortPos, inPortPos);

    // Verify connection was created
    QCOMPARE(TestUtils::countConnections(scene), 1);

    // Verify the connection is functional
    CircuitBuilder builder(scene);
    auto *sim = builder.initSimulation();
    sw->setOn(true);
    sim->update();
    QCOMPARE(TestUtils::getInputStatus(led), true);
}

void TestMainWindowGui::testRubberBandSelection()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *sw = new InputSwitch();
    auto *andGate = new And();
    auto *led = new Led();
    sw->setPos(100, 100);
    andGate->setPos(150, 100);
    led->setPos(200, 100);
    scene->addItem(sw);
    scene->addItem(andGate);
    scene->addItem(led);

    // Drag a selection rectangle encompassing all elements
    // Start above-left and end below-right of all elements
    dragElement(view, QPointF(50, 50), QPointF(250, 150));

    // All three elements should be selected
    QCOMPARE(scene->selectedElements().size(), 3);
}

void TestMainWindowGui::testDeleteConnection()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    auto *sw = new InputSwitch();
    auto *led = new Led();
    sw->setPos(50, 100);
    led->setPos(250, 100);
    scene->addItem(sw);
    scene->addItem(led);

    CircuitBuilder builder(scene);
    auto *conn = builder.connect(sw, 0, led, 0);
    QCOMPARE(TestUtils::countConnections(scene), 1);

    // Select the connection directly and delete via action
    conn->setSelected(true);
    scene->deleteAction();

    // Connection should be removed but elements remain
    QCOMPARE(TestUtils::countConnections(scene), 0);
    QCOMPARE(static_cast<int>(scene->elements().size()), 2);
}

void TestMainWindowGui::testCopyPasteConnectedCircuit()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    auto *sw = new InputSwitch();
    auto *led = new Led();
    sw->setPos(50, 100);
    led->setPos(250, 100);
    scene->addItem(sw);
    scene->addItem(led);

    CircuitBuilder builder(scene);
    builder.connect(sw, 0, led, 0);
    QCOMPARE(static_cast<int>(scene->elements().size()), 2);
    QCOMPARE(TestUtils::countConnections(scene), 1);

    // Select all, copy, paste via actions
    scene->selectAll();
    QCOMPARE(scene->selectedElements().size(), 2);

    scene->copyAction();
    scene->pasteAction();

    // Should have duplicated elements AND connections
    QCOMPARE(static_cast<int>(scene->elements().size()), 4);
    QCOMPARE(TestUtils::countConnections(scene), 2);
}

void TestMainWindowGui::testPropertyCycleMain()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    int initInputSize = andGate->inputSize();

    clickElement(view, andGate);

    // Cycle main property via scene API (input count for AND gate)
    scene->nextMainPropShortcut();
    QVERIFY(andGate->inputSize() != initInputSize);

    // Cycle back
    scene->prevMainPropShortcut();
    QCOMPARE(andGate->inputSize(), initInputSize);
}

void TestMainWindowGui::testToggleInputDuringSimulation()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *sw = new InputSwitch();
    auto *led = new Led();
    sw->setPos(50, 100);
    led->setPos(250, 100);
    scene->addItem(sw);
    scene->addItem(led);

    CircuitBuilder builder(scene);
    builder.connect(sw, 0, led, 0);
    auto *sim = builder.initSimulation();
    sim->update();

    // LED should be off initially
    QCOMPARE(TestUtils::getInputStatus(led), false);

    // Click the switch to toggle it
    clickElement(view, sw);

    // Update simulation
    sim->update();

    // After toggling, LED state depends on whether click toggles the switch.
    // InputSwitch toggles on click, so LED should now be on.
    QCOMPARE(TestUtils::getInputStatus(led), true);
}

void TestMainWindowGui::testInputButtonMomentary()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    auto *button = new InputButton();
    auto *led = new Led();
    button->setPos(50, 100);
    led->setPos(250, 100);
    scene->addItem(button);
    scene->addItem(led);

    CircuitBuilder builder(scene);
    builder.connect(button, 0, led, 0);
    auto *sim = builder.initSimulation();
    sim->update();

    // Button is off initially
    QCOMPARE(TestUtils::getInputStatus(led), false);

    // Simulate key press (button has default trigger key)
    button->setOn(true);
    sim->update();
    QCOMPARE(TestUtils::getInputStatus(led), true);

    // Release — button is momentary, should return to 0
    button->setOn(false);
    sim->update();
    QCOMPARE(TestUtils::getInputStatus(led), false);
}

void TestMainWindowGui::testRapidSimulationToggle()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *sim = window->currentTab()->simulation();
    bool initialRunning = sim->isRunning();

    // Rapidly toggle simulation 10 times
    for (int i = 0; i < 10; ++i) {
        QTest::keyClick(window.get(), Qt::Key_F5);
    }

    // After even number of toggles, state should match initial
    QCOMPARE(sim->isRunning(), initialRunning);

    // Scene should still be functional
    QVERIFY(window->currentTab()->scene() != nullptr);
}

void TestMainWindowGui::testOpenSameFileTwice()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());
    const int initialCount = tabs->count();

    window->loadPandaFile(m_fixtureDir + "/test_circuit.panda");
    QCOMPARE(tabs->count(), initialCount + 1);
    const int firstTabIndex = tabs->currentIndex();

    // Loading the same file again must reuse the existing tab, not open a new one.
    window->loadPandaFile(m_fixtureDir + "/test_circuit.panda");
    QCOMPARE(tabs->count(), initialCount + 1);
    QCOMPARE(tabs->currentIndex(), firstTabIndex);
}

void TestMainWindowGui::testOpenSameFileFromDifferentDirs()
{
    QTemporaryDir dir1, dir2;
    QVERIFY(dir1.isValid() && dir2.isValid());
    const QString path1 = dir1.path() + "/circuit.panda";
    const QString path2 = dir2.path() + "/circuit.panda";
    createMWFixture(path1);
    createMWFixture(path2);

    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());
    const int initialCount = tabs->count();

    window->loadPandaFile(path1);
    QCOMPARE(tabs->count(), initialCount + 1);

    // A different file that shares only the basename must open a separate tab.
    window->loadPandaFile(path2);
    QCOMPARE(tabs->count(), initialCount + 2);
}

// ===========================================================================
// Context menu operations
// ===========================================================================

void TestMainWindowGui::testContextMenuRotateLeft()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    clickElement(view, andGate);

    qreal before = andGate->rotation();
    scene->rotateLeft();
    QVERIFY(!qFuzzyCompare(andGate->rotation(), before));
}

void TestMainWindowGui::testContextMenuRotateRight()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    clickElement(view, andGate);

    qreal before = andGate->rotation();
    scene->rotateRight();
    QVERIFY(!qFuzzyCompare(andGate->rotation(), before));
}

void TestMainWindowGui::testContextMenuCopy()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *sw = new InputSwitch();
    sw->setPos(100, 100);
    scene->addItem(sw);
    clickElement(view, sw);
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);

    QTest::keyClick(window.get(), Qt::Key_C, Qt::ControlModifier);
    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), 2);
}

void TestMainWindowGui::testContextMenuCut()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *sw = new InputSwitch();
    sw->setPos(100, 100);
    scene->addItem(sw);
    clickElement(view, sw);
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);

    QTest::keyClick(window.get(), Qt::Key_X, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), 0);

    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);
}

void TestMainWindowGui::testContextMenuDelete()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *sw = new InputSwitch();
    auto *led = new Led();
    sw->setPos(100, 100);
    led->setPos(200, 100);
    scene->addItem(sw);
    scene->addItem(led);
    clickElement(view, sw);
    QCOMPARE(static_cast<int>(scene->elements().size()), 2);

    QTest::keyClick(window.get(), Qt::Key_Delete);
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);
}

void TestMainWindowGui::testContextMenuPasteOnEmpty()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    // First add and copy an element
    auto *sw = new InputSwitch();
    sw->setPos(100, 100);
    scene->addItem(sw);
    clickElement(view, sw);
    QTest::keyClick(window.get(), Qt::Key_C, Qt::ControlModifier);

    // Delete original
    QTest::keyClick(window.get(), Qt::Key_Delete);
    QCOMPARE(static_cast<int>(scene->elements().size()), 0);

    // Paste on empty canvas
    QTest::keyClick(window.get(), Qt::Key_V, Qt::ControlModifier);
    QCOMPARE(static_cast<int>(scene->elements().size()), 1);
}

void TestMainWindowGui::testContextMenuRename()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    // Use Led since it supports labels (And gates don't have hasLabel=true)
    auto *led = new Led();
    workspace.scene()->addItem(led);
    led->setSelected(true);
    QCoreApplication::processEvents();

    auto *labelEdit = editor.findChild<QLineEdit *>("lineEditElementLabel");
    QVERIFY2(labelEdit, "lineEditElementLabel not found");

    // Set label text via the ElementEditor widget (same path as context menu rename)
    labelEdit->setText("MyLED");
    QCoreApplication::processEvents();
    QCOMPARE(led->label(), QString("MyLED"));
}

void TestMainWindowGui::testContextMenuChangeTrigger()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *sw = new InputSwitch();
    workspace.scene()->addItem(sw);
    sw->setSelected(true);
    QCoreApplication::processEvents();

    auto *triggerEdit = editor.findChild<QLineEdit *>("lineEditTrigger");
    QVERIFY2(triggerEdit, "lineEditTrigger not found");

    editor.changeTriggerAction();
    QVERIFY(triggerEdit->hasFocus());
}

void TestMainWindowGui::testContextMenuChangeColor()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *andGate = new And();
    workspace.scene()->addItem(andGate);
    andGate->setSelected(true);
    QCoreApplication::processEvents();

    auto *colorCombo = editor.findChild<QComboBox *>("comboBoxColor");
    QVERIFY2(colorCombo, "comboBoxColor not found");
    QVERIFY(colorCombo->count() > 1);

    // Change color to index 1 (Red)
    colorCombo->setCurrentIndex(1);
    QCoreApplication::processEvents();
    QCOMPARE(colorCombo->currentIndex(), 1);
}

void TestMainWindowGui::testContextMenuChangeFrequency()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *clock = new Clock();
    workspace.scene()->addItem(clock);
    clock->setSelected(true);
    QCoreApplication::processEvents();

    auto *slider = editor.findChild<QSlider *>();
    if (!slider) {
        QSKIP("Frequency slider not found for Clock element");
    }

    int initial = slider->value();
    int target = (initial + slider->minimum() + 1 <= slider->maximum()) ? initial + 1 : initial - 1;
    slider->setValue(target);
    QCoreApplication::processEvents();
    QCOMPARE(slider->value(), target);
}

void TestMainWindowGui::testContextMenuMorphTo()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    clickElement(view, andGate);
    QVERIFY(andGate->isSelected());

    // Morph to next element via scene API (same as context menu Morph To)
    ElementType beforeType = andGate->elementType();
    scene->nextElm();

    // After morph, the original element is replaced — find the new one
    auto elements = scene->elements();
    QCOMPARE(static_cast<int>(elements.size()), 1);

    // The element type should have changed (And → next in Gate group)
    auto *morphed = elements.first();
    QVERIFY(morphed->elementType() != beforeType);
}

// ===========================================================================
// Keyboard shortcuts (additional)
// ===========================================================================

void TestMainWindowGui::testSearchFocusViaCtrlF()
{
    std::unique_ptr<MainWindow> window(createMW());

    auto *searchBox = window->findChild<QLineEdit *>("lineEditSearch");
    QVERIFY2(searchBox, "lineEditSearch not found");

    // Ctrl+F should focus the search box
    QTest::keyClick(window.get(), Qt::Key_F, Qt::ControlModifier);
    QVERIFY(searchBox->hasFocus());
}

void TestMainWindowGui::testMorphPrevNextViaKeyboard()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    auto *view = window->currentTab()->view();

    auto *andGate = new And();
    andGate->setPos(100, 100);
    scene->addItem(andGate);
    clickElement(view, andGate);

    ElementType beforeType = andGate->elementType();

    // Morph to next element via scene API (bound to '>' shortcut)
    scene->nextElm();

    auto elements = scene->elements();
    QCOMPARE(static_cast<int>(elements.size()), 1);
    auto *morphed = elements.first();
    QVERIFY(morphed->elementType() != beforeType);

    // Morph back via scene API (bound to '<' shortcut)
    ElementType afterType = morphed->elementType();
    clickElement(view, morphed);
    scene->prevElm();

    elements = scene->elements();
    QCOMPARE(static_cast<int>(elements.size()), 1);
    QVERIFY(elements.first()->elementType() != afterType);
}

// ===========================================================================
// Mouse interactions (additional)
// ===========================================================================

void TestMainWindowGui::testDoubleClickWireSplit()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    auto *sw = new InputSwitch();
    auto *led = new Led();
    sw->setPos(50, 100);
    led->setPos(350, 100);
    scene->addItem(sw);
    scene->addItem(led);

    CircuitBuilder builder(scene);
    auto *conn = builder.connect(sw, 0, led, 0);
    QCOMPARE(TestUtils::countConnections(scene), 1);
    QVERIFY(conn->startPort());
    QVERIFY(conn->endPort());

    // SplitCommand is what Scene::mouseDoubleClickEvent dispatches when
    // double-clicking a fully connected wire. Test the command directly
    // since hit-testing a thin Bezier curve is unreliable in headless mode.
    QPointF splitPos = (conn->startPort()->scenePos() + conn->endPort()->scenePos()) / 2.0;
    scene->receiveCommand(new SplitCommand(conn, splitPos, scene));

    // After split, there should be 2 connections and a Node element
    QVERIFY(TestUtils::countConnections(scene) >= 2);
    bool hasNode = false;
    for (auto *elm : scene->elements()) {
        if (elm->elementType() == ElementType::Node) {
            hasNode = true;
            break;
        }
    }
    QVERIFY2(hasNode, "No Node element created after wire split");
}

void TestMainWindowGui::testDragElementFromPalette()
{
    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();

    int initialCount = static_cast<int>(scene->elements().size());

    // Type into search box to find an element
    auto *searchBox = window->findChild<QLineEdit *>("lineEditSearch");
    QVERIFY2(searchBox, "lineEditSearch not found");

    // Set search text directly and trigger returnPressed — QTest::keyClicks
    // may not be accepted by the search box on headless/offscreen platforms.
    searchBox->setText("AND");
    QCoreApplication::processEvents();

    emit searchBox->returnPressed();
    QCoreApplication::processEvents();

    // An element should have been added
    QVERIFY(static_cast<int>(scene->elements().size()) > initialCount);
}

// ===========================================================================
// View/Help operations
// ===========================================================================

void TestMainWindowGui::testThemeSwitching()
{
    std::unique_ptr<MainWindow> window(createMW());

    auto *darkAction = window->findChild<QAction *>("actionDarkTheme");
    auto *lightAction = window->findChild<QAction *>("actionLightTheme");
    auto *systemAction = window->findChild<QAction *>("actionSystemTheme");
    QVERIFY2(darkAction, "actionDarkTheme not found");
    QVERIFY2(lightAction, "actionLightTheme not found");
    QVERIFY2(systemAction, "actionSystemTheme not found");

    darkAction->trigger();
    QCOMPARE(ThemeManager::theme(), Theme::Dark);

    lightAction->trigger();
    QCOMPARE(ThemeManager::theme(), Theme::Light);

    systemAction->trigger();
    QCOMPARE(ThemeManager::theme(), Theme::System);

    // Restore to light for other tests
    lightAction->trigger();
}

void TestMainWindowGui::testAboutThisVersionDialog()
{
    std::unique_ptr<MainWindow> window(createMW());

    auto *action = window->findChild<QAction *>("actionAboutThisVersion");
    QVERIFY2(action, "actionAboutThisVersion not found");

    autoCloseNextMessageBox();
    action->trigger();

    // If we reached here without blocking, the dialog was auto-closed successfully
    QVERIFY(true);
}

void TestMainWindowGui::testOpenExample()
{
    std::unique_ptr<MainWindow> window(createMW());

    auto *menuExamples = window->findChild<QMenu *>("menuExamples");
    if (!menuExamples || menuExamples->actions().isEmpty()) {
        QSKIP("Examples menu not found or empty (no Examples/ directory in test environment)");
    }

    auto *tabs = findTabs(window.get());
    int initialCount = tabs->count();

    // Trigger the first example
    menuExamples->actions().first()->trigger();
    QVERIFY(tabs->count() > initialCount);
}

void TestMainWindowGui::testMakeSelfContained()
{
    std::unique_ptr<MainWindow> window(createMW());

    auto *action = window->findChild<QAction *>("actionMakeSelfContained");
    QVERIFY2(action, "actionMakeSelfContained not found");

    // On an empty unsaved project, makeSelfContained should show a warning
    // or do nothing (no file-backed ICs). Auto-close any message box.
    autoCloseNextMessageBox();
    action->trigger();

    // Should not crash; scene still functional
    QVERIFY(window->currentTab()->scene() != nullptr);
}

// ===========================================================================
// ElementEditor operations
// ===========================================================================

void TestMainWindowGui::testElementLabelRename()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *led = new Led();
    workspace.scene()->addItem(led);
    led->setSelected(true);
    QCoreApplication::processEvents();

    auto *labelEdit = editor.findChild<QLineEdit *>("lineEditElementLabel");
    QVERIFY2(labelEdit, "lineEditElementLabel not found");

    labelEdit->setText("TestLabel123");
    QCoreApplication::processEvents();
    QCOMPARE(led->label(), QString("TestLabel123"));
}

void TestMainWindowGui::testElementColorChange()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *andGate = new And();
    workspace.scene()->addItem(andGate);
    andGate->setSelected(true);
    QCoreApplication::processEvents();

    auto *colorCombo = editor.findChild<QComboBox *>("comboBoxColor");
    QVERIFY2(colorCombo, "comboBoxColor not found");

    if (colorCombo->count() < 2) {
        QSKIP("Color combo has fewer than 2 entries");
    }

    colorCombo->setCurrentIndex(0);
    QCoreApplication::processEvents();
    QString firstColor = colorCombo->currentText();

    colorCombo->setCurrentIndex(1);
    QCoreApplication::processEvents();
    QVERIFY(colorCombo->currentText() != firstColor);
}

void TestMainWindowGui::testElementInputCountChange()
{
    WorkSpace workspace;
    ElementEditor editor;
    editor.setScene(workspace.scene());

    auto *andGate = new And();
    workspace.scene()->addItem(andGate);
    andGate->setSelected(true);
    QCoreApplication::processEvents();

    auto *inputCombo = editor.findChild<QComboBox *>("comboBoxInputSize");
    QVERIFY2(inputCombo, "comboBoxInputSize not found");

    if (inputCombo->count() < 2) {
        QSKIP("Input size combo has fewer than 2 entries");
    }

    int initialInputs = andGate->inputSize();
    // Select a different input count
    int targetIdx = (inputCombo->currentIndex() + 1) % inputCombo->count();
    inputCombo->setCurrentIndex(targetIdx);
    QCoreApplication::processEvents();

    QVERIFY(andGate->inputSize() != initialInputs);
}

// ===========================================================================
// Embedded IC: embedICByFile / extractICByBlobName
// ===========================================================================

static QByteArray makeICDropMime(ElementType type, const QString &icFileName,
                                 bool isEmbedded, const QString &blobName = {})
{
    QByteArray data;
    QDataStream stream(&data, QIODevice::WriteOnly);
    Serialization::writePandaHeader(stream);
    stream << QPoint(32, 32) << type << icFileName << isEmbedded
           << (isEmbedded ? blobName : QString());
    return data;
}

static void simulateDrop(QWidget *dropZone, const QByteArray &mimePayload)
{
    QMimeData mime;
    mime.setData("application/x-wiredpanda-dragdrop", mimePayload);

    QDragEnterEvent enterEv(QPoint(10, 10), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier);
    QCoreApplication::sendEvent(dropZone, &enterEv);

    QDropEvent dropEv(QPointF(10, 10), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier);
    QCoreApplication::sendEvent(dropZone, &dropEv);
}

void TestMainWindowGui::testEmbedICByFileNoInstances()
{
    // When a file-based IC is dragged to the embedded palette zone and no instances
    // of that file exist in the scene, embedICByFile must register the blob
    // (RegisterBlobCommand) without adding any IC element to the scene.

    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    auto *dropZone = window->findChild<QWidget *>("dropZoneEmbedded");
    QVERIFY2(dropZone, "dropZoneEmbedded widget not found");

    const int elementsBefore = static_cast<int>(scene->elements().size());

    simulateDrop(dropZone, makeICDropMime(ElementType::IC, "test_circuit.panda", false));

    QCOMPARE(scene->elements().size(), elementsBefore); // no IC instantiated
    QVERIFY(scene->icRegistry()->hasBlob("test_circuit"));
    QVERIFY(scene->undoStack()->canUndo());

    scene->undoStack()->undo();
    QVERIFY(!scene->icRegistry()->hasBlob("test_circuit"));
    QCOMPARE(scene->elements().size(), elementsBefore);
}

void TestMainWindowGui::testEmbedICByFileWithInstances()
{
    // When file-backed ICs referencing the file already exist in the scene,
    // a drop on the embedded palette zone must convert them to embedded without
    // adding extra elements.

    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    auto *dropZone = window->findChild<QWidget *>("dropZoneEmbedded");
    QVERIFY2(dropZone, "dropZoneEmbedded widget not found");

    auto *ic1 = new IC();
    ic1->loadFile("test_circuit.panda", m_fixtureDir);
    ic1->setPos(100, 100);
    scene->addItem(ic1);

    auto *ic2 = new IC();
    ic2->loadFile("test_circuit.panda", m_fixtureDir);
    ic2->setPos(200, 100);
    scene->addItem(ic2);

    const int elementsBefore = static_cast<int>(scene->elements().size());
    QVERIFY(!ic1->isEmbedded());
    QVERIFY(!ic2->isEmbedded());

    simulateDrop(dropZone, makeICDropMime(ElementType::IC, "test_circuit.panda", false));

    QCOMPARE(scene->elements().size(), elementsBefore); // conversion, not addition
    QVERIFY(ic1->isEmbedded());
    QVERIFY(ic2->isEmbedded());
    QVERIFY(scene->icRegistry()->hasBlob("test_circuit"));

    scene->undoStack()->undo();
    QVERIFY(!ic1->isEmbedded());
    QVERIFY(!ic2->isEmbedded());
    QVERIFY(!scene->icRegistry()->hasBlob("test_circuit"));
}

void TestMainWindowGui::testExtractICByBlobNameEndToEnd()
{
    // Dragging an embedded IC to the file-based palette zone must write the blob
    // to the dialog-chosen path, remove it from the registry, and convert the IC
    // to file-backed. Undo must reinstate the embedded state.

    std::unique_ptr<MainWindow> window(createMW());
    auto *scene = window->currentTab()->scene();
    scene->setContextDir(m_fixtureDir);

    auto *dropZone = window->findChild<QWidget *>("dropZoneFileBased");
    QVERIFY2(dropZone, "dropZoneFileBased widget not found");

    auto *ic = placeEmbeddedIC(scene, m_fixtureDir, "extract_test");
    QVERIFY(ic->isEmbedded());
    QVERIFY(scene->icRegistry()->hasBlob("extract_test"));

    const QString outPath = m_fixtureDir + "/extracted_output.panda";
    QFile::remove(outPath);

    ScopedFileDialogStub guard;
    guard.stub.saveResult = {outPath, "Panda files (*.panda)"};

    simulateDrop(dropZone, makeICDropMime(ElementType::IC, "extract_test", true, "extract_test"));

    QVERIFY2(QFile::exists(outPath), "Extracted file should exist on disk");
    QVERIFY2(QFile(outPath).size() > 0, "Extracted file should be non-empty");
    QVERIFY(!scene->icRegistry()->hasBlob("extract_test"));
    QVERIFY(!ic->isEmbedded());
    QVERIFY2(ic->file().contains("extracted_output"), "IC file path should point to extracted file");

    scene->undoStack()->undo();
    QVERIFY(ic->isEmbedded());
    QVERIFY(scene->icRegistry()->hasBlob("extract_test"));

    QFile::remove(outPath);
}

// ===========================================================================
// Sentry triage regressions
// ===========================================================================

void TestMainWindowGui::testSetupShortcutsRegistersOnceD18()
{
    // Pre-fix the constructor called setupShortcuts() twice, leaking the
    // first batch's seven QShortcut objects and registering [, ], {, }, <, >
    // under two QShortcut children each.
    std::unique_ptr<MainWindow> window(createMW());

    const QList<QKeySequence> bracketKeys = {
        QKeySequence("["),
        QKeySequence("]"),
        QKeySequence("{"),
        QKeySequence("}"),
        QKeySequence("<"),
        QKeySequence(">"),
    };

    const auto shortcuts = window->findChildren<QShortcut *>();
    for (const auto &key : bracketKeys) {
        int matches = 0;
        for (auto *sc : shortcuts) {
            if (sc->key() == key) {
                ++matches;
            }
        }
        QCOMPARE(matches, 1);
    }
}

void TestMainWindowGui::testAddICDisabledWhenUnsavedC9()
{
    // The Add IC button needs a saved-project directory to copy the chosen
    // .panda into. Pre-fix it was always clickable and the click threw
    // "Save file first." straight into a modal error.
    std::unique_ptr<MainWindow> window(createMW());

    auto *addBtn = window->findChild<QPushButton *>("pushButtonAddIC");
    QVERIFY(addBtn);

    // Fresh tab — fileInfo is not readable, so Add IC is gated off.
    QVERIFY(!window->currentTab()->fileInfo().isReadable());
    QVERIFY(!addBtn->isEnabled());

    // Saving the project flips the precondition; the button must enable.
    StubFileDialogProvider stub;
    stub.saveResult = {m_fixtureDir + "/c9_proj.panda", "Panda files (*.panda)"};
    auto *prevProvider = FileDialogs::setProvider(&stub);

    window->currentTab()->save({});
    QVERIFY(window->currentTab()->fileInfo().isReadable());

    QVERIFY(addBtn->isEnabled());

    FileDialogs::setProvider(prevProvider);
}

void TestMainWindowGui::testRotateFlipZoomBreadcrumbsB23()
{
    // The behavior change is "MainWindow's Rotate/Flip/Zoom slots stop
    // emitting their own sentryBreadcrumb because Scene/View already do".
    // sentryBreadcrumb is a no-op without HAVE_SENTRY linked, so we verify
    // the regression at the source level: the seven affected slots must not
    // contain a sentryBreadcrumb call any more.
    // CURRENTDIR is defined at compile time as ${CMAKE_CURRENT_SOURCE_DIR}/Tests
    const QString sourcePath = QString(QUOTE(CURRENTDIR)) + "/../App/UI/MainWindow.cpp";
    QFile src(sourcePath);
    QVERIFY2(src.open(QIODevice::ReadOnly),
             qPrintable(QString("Cannot open %1").arg(src.fileName())));
    const QString source = QString::fromUtf8(src.readAll());
    src.close();

    const QStringList slotsThatLostBreadcrumbs = {
        "MainWindow::on_actionRotateRight_triggered",
        "MainWindow::on_actionRotateLeft_triggered",
        "MainWindow::on_actionFlipHorizontally_triggered",
        "MainWindow::on_actionFlipVertically_triggered",
        "MainWindow::on_actionZoomIn_triggered",
        "MainWindow::on_actionZoomOut_triggered",
        "MainWindow::on_actionResetZoom_triggered",
    };

    for (const QString &slotName : slotsThatLostBreadcrumbs) {
        const qsizetype start = source.indexOf(slotName);
        QVERIFY2(start >= 0, qPrintable("Could not find " + slotName));

        // Walk to the opening brace then balance to the closing brace.
        const qsizetype openBrace = source.indexOf('{', start);
        QVERIFY(openBrace > start);
        int depth = 0;
        qsizetype closeBrace = -1;
        for (qsizetype i = openBrace; i < source.size(); ++i) {
            if (source[i] == '{') ++depth;
            else if (source[i] == '}') {
                --depth;
                if (depth == 0) { closeBrace = i; break; }
            }
        }
        QVERIFY(closeBrace > openBrace);
        const QString body = source.mid(openBrace, closeBrace - openBrace + 1);

        QVERIFY2(!body.contains("sentryBreadcrumb"),
                 qPrintable(slotName + " still emits sentryBreadcrumb — Scene/View "
                            "already emits a richer breadcrumb for the same action; "
                            "MainWindow's emit duplicates it and pollutes the buffer."));
    }
}

void TestMainWindowGui::testLoadPandaFileClosesOrphanedTabB11()
{
    // Pre-fix loadPandaFile created a tab unconditionally, then called load().
    // When load threw (corrupt file, missing IC dep, garbage trailing tail
    // from the autosave bug) the empty tab stayed in the tab bar — three bad
    // autosaves at startup left three orphan empty tabs. The try/catch +
    // closeTab in B11 keeps the tab count flat across a failed load.
    QTemporaryDir tmp;
    QVERIFY(tmp.isValid());
    // A non-existent file is the simplest path that makes WorkSpace::load
    // throw — it covers the bug class (load throws AFTER the tab is added)
    // without depending on the parser's exact failure mode for malformed bytes.
    const QString badPath = tmp.path() + "/missing.panda";

    std::unique_ptr<MainWindow> window(createMW());
    auto *tabs = findTabs(window.get());
    QVERIFY(tabs);
    const int countBefore = tabs->count();

    bool threw = false;
    try {
        window->loadPandaFile(badPath);
    } catch (const std::exception &) {
        threw = true;
    }

    QVERIFY2(threw, "Expected loadPandaFile to propagate the parse failure");
    QCOMPARE(tabs->count(), countBefore);
}

void TestMainWindowGui::testRemoveICFileIsUndoableA14()
{
    // Pre-fix removeICFile freed each matching IC via removeItem + delete and
    // never touched the undo stack — undo couldn't bring the IC back, and the
    // freed pointer stayed in m_sortedElements until the next sim tick faulted
    // on it (the H2-shape escape #1 from the triage). The fix routes through
    // DeleteItemsCommand + simulation->restart() so undo works and m_sortedElements
    // is rebuilt cleanly.
    QTemporaryDir tmp;
    QVERIFY(tmp.isValid());
    const QString projPath = tmp.path() + "/proj.panda";
    const QString icPath = tmp.path() + "/sub.panda";
    createMWFixture(projPath);
    createMWFixture(icPath);

    StubFileDialogProvider stub;
    stub.openResult = projPath;
    stub.saveResult = {projPath, "Panda files (*.panda)"};
    auto *prevProvider = FileDialogs::setProvider(&stub);
    const bool prevInteractive = Application::interactiveMode;
    Application::interactiveMode = false;

    std::unique_ptr<MainWindow> window(createMW());
    window->loadPandaFile(projPath);

    auto *scene = window->currentTab()->scene();
    auto *ic = new IC();
    ic->loadFile(icPath, tmp.path());
    ic->setLabel("sub");  // label + ".panda" must equal the icFileName arg
    scene->addItem(ic);

    auto countMatchingICs = [&]() {
        int n = 0;
        for (auto *elm : scene->elements()) {
            if (elm->elementType() == ElementType::IC
                && elm->label().append(".panda").toLower() == "sub.panda") {
                ++n;
            }
        }
        return n;
    };

    auto *undoStack = scene->undoStack();
    const int stackBefore = undoStack->count();

    QCOMPARE(countMatchingICs(), 1);

    // removeICFile is private; the user-facing trigger is the TrashButton
    // (pushButtonRemoveIC) emitting its removeICFile signal. Find the
    // button and emit the signal directly to drive the same code path.
    auto *trashBtn = window->findChild<TrashButton *>("pushButtonRemoveIC");
    QVERIFY(trashBtn);
    emit trashBtn->removeICFile("sub.panda");

    QCOMPARE(countMatchingICs(), 0);
    QVERIFY2(!QFile::exists(icPath), "removeICFile must delete the .panda file");

    // The load-bearing regression: the IC removal went through the undo
    // stack — pre-fix it was a bare removeItem + delete with no command
    // pushed. The file-restoration on undo is out of scope (the .panda was
    // deleted by QFile::remove and isn't part of the undo command).
    QVERIFY2(undoStack->count() > stackBefore,
             "removeICFile must push a DeleteItemsCommand on the undo stack");

    Application::interactiveMode = prevInteractive;
    FileDialogs::setProvider(prevProvider);
}

void TestMainWindowGui::testWaveformOnEmptySceneShowsWarningHQ()
{
    // Regression for WIREDPANDA-HQ on macOS arm64.
    //
    // Pre-fix: triggering actionWaveform on an empty scene drove
    // DolphinModelBuilder::collect() to throw PANDACEPTION. On macOS arm64
    // the exception escaped Application::notify and the process aborted with
    // SIGABRT (captured as a Sentry minidump). The fix at
    // MainWindow::on_actionWaveform_triggered pre-checks the empty case and
    // shows a warning dialog instead of letting the throw escape.
    //
    // This GUI variant exercises the exact user path. The trigger is posted
    // via Qt::QueuedConnection so the slot is invoked through the platform
    // event dispatcher — on macOS that is QCocoaEventDispatcherPrivate
    // ::postedEventsSourceCallback, the same dispatch frame as the original
    // crash. A direct action->trigger() would call the slot synchronously
    // and bypass Application::notify entirely.
    //
    // The non-GUI catch infrastructure is verified separately by
    // TestNotifyCatch; this test exercises the path with a real MainWindow.
    const bool prevInteractive = Application::interactiveMode;
    Application::interactiveMode = true;

    std::unique_ptr<MainWindow> window(createMW());

    QVERIFY2(window->currentTab()->scene()->elements().isEmpty(),
             "Fresh tab must have an empty scene to reproduce WIREDPANDA-HQ");

    // Poll-and-dismiss any QMessageBox that appears: either the pre-check
    // warning (with the fix) or the critical dialog Application::notify
    // shows from inside its catch (without the fix). A singleShot(0) won't
    // work — it fires before the queued action trigger runs, so it sees no
    // dialog and we hang on QDialog::exec.
    QTimer dismissTimer;
    dismissTimer.setInterval(50);
    QObject::connect(&dismissTimer, &QTimer::timeout, []() {
        const auto widgets = QApplication::topLevelWidgets();
        for (QWidget *w : widgets) {
            if (auto *box = qobject_cast<QMessageBox *>(w)) {
                if (box->isVisible()) {
                    box->done(QMessageBox::Ok);
                }
            }
        }
    });
    dismissTimer.start();

    auto *action = window->findChild<QAction *>("actionWaveform");
    QVERIFY(action);
    QMetaObject::invokeMethod(action, "trigger", Qt::QueuedConnection);
    QTest::qWait(500);

    dismissTimer.stop();

    // Reaching this line means: no abort. Either the pre-check fired (fix
    // present) or Application::notify caught the escaping PANDACEPTION
    // (without the fix). On macOS, a SIGABRT here is the WIREDPANDA-HQ
    // signature.
    QVERIFY(window->isVisible());

    Application::interactiveMode = prevInteractive;
}
