/*  KStars UI tests
    SPDX-FileCopyrightText: 2020 Eric Dejouhanet <eric.dejouhanet@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/


#include "test_ekos_focus.h"

#if defined(HAVE_INDI)

#include "kstars_ui_tests.h"
#include "test_ekos.h"
#include "test_ekos_helper.h"
#include "test_ekos_simulator.h"
#include "test_ekos_mount.h"
#include "ekos/focus/focusmodule.h"
#include "ekos/focus/focusutils.h"
#include "Options.h"

namespace
{
int focusDetectedStars()
{
    QLabel *starsOut = Ekos::Manager::Instance()->focusModule()->mainFocuser().get()->findChild<QLabel *>("starsOut");
    if (starsOut == nullptr)
        return -1;

    bool ok = false;
    const int starsDetected = starsOut->text().toInt(&ok);
    return ok ? starsDetected : -1;
}
}

class KFocusProcedureSteps: public QObject
{
    public:
        QMetaObject::Connection starting;
        QMetaObject::Connection aborting;
        QMetaObject::Connection completing;
        QMetaObject::Connection notguiding;
        QMetaObject::Connection guiding;
        QMetaObject::Connection quantifying;

    public:
        bool started { false };
        bool aborted { false };
        bool complete { false };
        bool unguided { false };
        bool ran_once { false };
        double hfr { -2 };

    public:
        KFocusProcedureSteps():
            starting (connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().get(), &Ekos::Focus::autofocusStarting,
                              this, &KFocusProcedureSteps::handleAutofocusStarting, Qt::UniqueConnection)),
            aborting (connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().get(), &Ekos::Focus::autofocusAborted,
                              this, &KFocusProcedureSteps::handleAutofocusAborted, Qt::UniqueConnection)),
            completing (connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().get(), &Ekos::Focus::autofocusComplete,
                                this, &KFocusProcedureSteps::handleAutofocusComplete, Qt::UniqueConnection)),
            notguiding (connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().get(), &Ekos::Focus::suspendGuiding,
                                this, &KFocusProcedureSteps::handleSuspendGuiding, Qt::UniqueConnection)),
            guiding (connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().get(), &Ekos::Focus::resumeGuiding,
                             this, &KFocusProcedureSteps::handleResumeGuiding, Qt::UniqueConnection)),
            quantifying (connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().get(), &Ekos::Focus::newHFR,
                                 this, &KFocusProcedureSteps::handleNewHFR))
        {};
        virtual ~KFocusProcedureSteps()
        {
            disconnect(starting);
            disconnect(aborting);
            disconnect(completing);
            disconnect(notguiding);
            disconnect(guiding);
            disconnect(quantifying);
        };
    public slots:
        void handleAutofocusCompleteRestartOnce()
        {
            complete = true;
            started = false;
            if (!ran_once)
            {
                Ekos::Manager::Instance()->focusModule()->mainFocuser()->start();
                ran_once = true;
            }
        }
        void handleAutofocusAbortedRestartOnce()
        {
            aborted = true;
            started = false;
            if (!ran_once)
            {
                Ekos::Manager::Instance()->focusModule()->mainFocuser()->start();
                ran_once = true;
            }
        }

    private slots:
        void handleAutofocusStarting()
        {
            started = true;
        }
        void handleAutofocusAborted()
        {
            started = false;
            aborted = true;
        }
        void handleAutofocusComplete()
        {
            started = false;
            complete = true;
        }
        void handleSuspendGuiding()
        {
            unguided = true;
        }
        void handleResumeGuiding()
        {
            unguided = false;
        }
        void handleNewHFR(double _hfr)
        {
            hfr = _hfr;
        }
};

class KFocusStateList: public QObject, public QList <Ekos::FocusState>
{
    public:
        QMetaObject::Connection handler;

    public:
        KFocusStateList():
            handler (connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().get(), &Ekos::Focus::newStatus,
                             this, &KFocusStateList::onNewState, Qt::UniqueConnection))
        {};
        virtual ~KFocusStateList() {};

    private slots:
        void onNewState(Ekos::FocusState s, const QString &)
        {
            append(s);
        }
};

TestEkosFocus::TestEkosFocus(QObject *parent) : QObject(parent)
{
}

void TestEkosFocus::initTestCase()
{
    KVERIFY_EKOS_IS_HIDDEN();
    KTRY_OPEN_EKOS();
    KVERIFY_EKOS_IS_OPENED();
    KTRY_EKOS_START_SIMULATORS();

    TestEkosHelper *helper = new TestEkosHelper();
    helper->init();
    // set the focuser device for correct preparation of optical trains
    helper->m_FocuserDevice = "Focuser Simulator";
    // prepare the optical trains
    helper->prepareOpticalTrains();
    // prepare the mount module (this has the side effect that it sets focal length and apperture in CCD Simulator)
    helper->prepareMountModule(TestEkosHelper::SCOPE_NEWTON_10F4, TestEkosHelper::SCOPE_TAKFINDER10x50);

    // We can't use this here because of the meridian flip test
    // HACK: Reset clock to initial conditions
    // KHACK_RESET_EKOS_TIME();

    KTELL_BEGIN();
}

void TestEkosFocus::cleanupTestCase()
{
    KTELL_END();
    KTRY_EKOS_STOP_SIMULATORS();
    KTRY_CLOSE_EKOS();
    KVERIFY_EKOS_IS_HIDDEN();
}

void TestEkosFocus::init()
{
}

void TestEkosFocus::cleanup()
{
    if (Ekos::Manager::Instance())
        if (Ekos::Manager::Instance()->focusModule())
            Ekos::Manager::Instance()->focusModule()->mainFocuser()->abort();
    KTELL_HIDE();
}

void TestEkosFocus::testCaptureStates()
{
    KTELL("Sync high on meridian to avoid jitter in CCD Simulator.");
    KTRY_MOUNT_SYNC(60.0, true, -1);

    // Prepare to detect state change
    KFocusStateList state_list;
    QVERIFY(state_list.handler);

    KTELL("Configure fields.\nCapture a frame.\nExpect PROGRESS, IDLE.");
    KTRY_FOCUS_MOVETO(40000);
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0);
    KTRY_FOCUS_DETECT(2, 1, 99);
    if (focusDetectedStars() <= 0)
        QSKIP("CCD simulator reported no stars; star-dependent focus state assertions require detectable stars.");
    QTRY_COMPARE_WITH_TIMEOUT(state_list.count(), 2, 5000);
    QCOMPARE(state_list[0], Ekos::FocusState::FOCUS_PROGRESS);
    QCOMPARE(state_list[1], Ekos::FocusState::FOCUS_IDLE);
    state_list.clear();

    KTELL("Move focuser.\nExpect no capture triggered.");
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0);
    KTRY_FOCUS_MOVETO(43210);
    QTest::qWait(1000);
    QCOMPARE(state_list.count(), 0);

    KTRY_FOCUS_GADGET(QPushButton, startLoopB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);

    KTELL("Loop captures.\nAbort loop.\nExpect FRAMING, PROGRESS, ABORTED, ABORTED.");
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0);
    KTRY_FOCUS_CLICK(startLoopB);
    QTRY_VERIFY_WITH_TIMEOUT(state_list.count() >= 1, 5000);
    KTRY_FOCUS_CLICK(stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(state_list.count() >= 4, 5000);
    QCOMPARE((int)state_list[0], (int)Ekos::FocusState::FOCUS_FRAMING);
    QCOMPARE((int)state_list[1], (int)Ekos::FocusState::FOCUS_PROGRESS);
    QCOMPARE((int)state_list[2], (int)Ekos::FocusState::FOCUS_ABORTED);
    QCOMPARE((int)state_list[3], (int)Ekos::FocusState::FOCUS_ABORTED);
    state_list.clear();

    KTRY_FOCUS_GADGET(QCheckBox, focusAutoStarEnabled);
    KTRY_FOCUS_GADGET(QPushButton, resetFrameB);

    QWARN("This test does not wait for the hardcoded timeout to select a star.");

    KTELL("Use a successful automatic star selection (not full-field).\nCapture a frame.\nExpect PROGRESS, IDLE\nCheck star selection.");
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 0.0, 3.0);
    focusAutoStarEnabled->setCheckState(Qt::CheckState::Checked);
    KTRY_FOCUS_DETECT(2, 1, 99);
    QTRY_VERIFY_WITH_TIMEOUT(state_list.count() >= 2, 5000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);
    KTRY_FOCUS_CLICK(resetFrameB);
    QCOMPARE(state_list[0], Ekos::FocusState::FOCUS_PROGRESS);
    QCOMPARE(state_list[1], Ekos::FocusState::FOCUS_IDLE);
    focusAutoStarEnabled->setCheckState(Qt::CheckState::Unchecked);
    state_list.clear();

    KTELL("Use an unsuccessful automatic star selection (not full-field).\nCapture a frame\nExpect PROGRESS, IDLE.\nCheck star selection.");
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 0.0, 3.0);
    focusAutoStarEnabled->setCheckState(Qt::CheckState::Checked);
    KTRY_FOCUS_DETECT(0.01, 1, 1);
    QTRY_VERIFY_WITH_TIMEOUT(state_list.count() >= 2, 5000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);
    KTRY_FOCUS_CLICK(resetFrameB);
    QCOMPARE(state_list[0], Ekos::FocusState::FOCUS_PROGRESS);
    QCOMPARE(state_list[1], Ekos::FocusState::FOCUS_WAITING);
    focusAutoStarEnabled->setCheckState(Qt::CheckState::Unchecked);
    state_list.clear();
}

void TestEkosFocus::testDuplicateFocusRequest()
{
    KTELL("Sync high on meridian to avoid jitter in CCD Simulator.\nConfigure a fast autofocus.");
    KTRY_FOCUS_SHOW();
    KTRY_MOUNT_SYNC(60.0, true, -1);
    KTRY_FOCUS_MOVETO(35000);
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 0.0, 30);
    KTRY_FOCUS_EXPOSURE(3, 99);

    KTRY_FOCUS_GADGET(QPushButton, startFocusB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);

    // Prepare to detect the beginning of the autofocus_procedure
    KFocusProcedureSteps autofocus;
    QVERIFY(autofocus.starting);

    KTELL("Click the autofocus button\nExpect a signal that the procedure starts.\nExpect state change and disabled button.");
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QVERIFY(Ekos::Manager::Instance()->focusModule()->mainFocuser()->status() != Ekos::FOCUS_IDLE);
    QVERIFY(!startFocusB->isEnabled());
    QVERIFY(stopFocusB->isEnabled());

    KTELL("Issue a few autofocus commands at that point through the d-bus entry point\nExpect no parallel procedure start.");
    for (int i = 0; i < 5; i++)
    {
        autofocus.started = false;
        Ekos::Manager::Instance()->focusModule()->mainFocuser()->start();
        QTest::qWait(500);
        QVERIFY(!autofocus.started);
    }

    KTELL("Stop the running autofocus.");
    KTRY_FOCUS_CLICK(stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 5000);
}

void TestEkosFocus::testAutofocusSignalEmission()
{
    KTELL("Sync high on meridian to avoid jitter in CCD Simulator.\nConfigure fast autofocus.");
    KTRY_FOCUS_SHOW();
    KTRY_MOUNT_SYNC(60.0, true, -1);

    KTRY_FOCUS_MOVETO(35000);
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 30);
    KTRY_FOCUS_EXPOSURE(3, 99);
    KTRY_FOCUS_DETECT(3, 1, 99);
    if (focusDetectedStars() <= 0)
        QSKIP("CCD simulator reported no stars; autofocus completion assertions require detectable stars.");

    KTRY_FOCUS_GADGET(QPushButton, startFocusB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);

    // Prepare to detect the beginning of the autofocus_procedure
    KFocusProcedureSteps autofocus;
    QVERIFY(autofocus.starting);

    KTELL("Configure to restart autofocus when it finishes, like Scheduler does.");
    autofocus.completing = connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().data(),
                                   &Ekos::Focus::autofocusComplete,
                                   &autofocus, &KFocusProcedureSteps::handleAutofocusCompleteRestartOnce, Qt::UniqueConnection);
    QVERIFY(autofocus.completing);

    KTELL("Run autofocus, wait for completion.\nHandler restarts a second one immediately.");
    QVERIFY(!autofocus.started);
    QVERIFY(!autofocus.complete);
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.complete, 60000);

    KTELL("Wait for the second run to finish.\nNo other autofocus started.");
    autofocus.complete = false;
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.complete, 60000);
    QVERIFY(!autofocus.started);
}

void TestEkosFocus::testFocusAbort()
{
    KTELL("Sync high on meridian to avoid jitter in CCD Simulator.\nConfigure fast autofocus.");
    KTRY_FOCUS_SHOW();
    KTRY_MOUNT_SYNC(60.0, true, -1);
    KTRY_FOCUS_MOVETO(35000);
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 30);
    KTRY_FOCUS_EXPOSURE(3, 99);
    KTRY_FOCUS_DETECT(3, 1, 99);
    if (focusDetectedStars() <= 0)
        QSKIP("CCD simulator reported no stars; autofocus abort assertions require detectable stars.");

    KTRY_FOCUS_GADGET(QPushButton, startFocusB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);

    // Prepare to detect the beginning of the autofocus_procedure
    KFocusProcedureSteps autofocus;
    QVERIFY(autofocus.starting);
    QVERIFY(autofocus.completing);

    KTELL("Configure to restart autofocus when it finishes, like Scheduler does.");
    autofocus.aborting = connect(Ekos::Manager::Instance()->focusModule()->mainFocuser().data(), &Ekos::Focus::autofocusAborted,
                                 &autofocus, &KFocusProcedureSteps::handleAutofocusAbortedRestartOnce, Qt::UniqueConnection);
    QVERIFY(autofocus.aborting);

    KTELL("Run autofocus, don't wait for the completion signal and abort it.\nHandler restarts a second one immediately.");
    QVERIFY(!autofocus.started);
    QVERIFY(!autofocus.aborted);
    QVERIFY(!autofocus.complete);
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    KTRY_FOCUS_CLICK(stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 5000);

    KTELL("Wait for the second run to finish.\nNo other autofocus started.");
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.complete, 60000);
    QVERIFY(!autofocus.started);
}

void TestEkosFocus::testGuidingSuspendWhileFocusing()
{
    KTELL("Sync high on meridian to avoid jitter in CCD Simulator\nConfigure a fast autofocus.");
    KTRY_FOCUS_SHOW();
    KTRY_MOUNT_SYNC(60.0, true, -1);
    KTRY_FOCUS_MOVETO(35000);
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 30);
    KTRY_FOCUS_EXPOSURE(3, 99);
    KTRY_FOCUS_DETECT(3, 1, 99);
    if (focusDetectedStars() <= 0)
        QSKIP("CCD simulator reported no stars; guiding-suspend focus assertions require detectable stars.");

    KTRY_FOCUS_GADGET(QPushButton, startFocusB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);

    // Prepare to detect the beginning of the autofocus_procedure
    KFocusProcedureSteps autofocus;
    QVERIFY(autofocus.starting);
    QVERIFY(autofocus.aborting);
    QVERIFY(autofocus.completing);
    QVERIFY(autofocus.notguiding);
    QVERIFY(autofocus.guiding);

    KTRY_FOCUS_GADGET(QCheckBox, focusSuspendGuiding);

    KTELL("Abort the autofocus with guiding set to suspend\nGuiding required to suspend, then required to resume");
    focusSuspendGuiding->setCheckState(Qt::CheckState::Checked);
    QVERIFY(!autofocus.started);
    QVERIFY(!autofocus.aborted);
    QVERIFY(!autofocus.complete);
    QVERIFY(!autofocus.unguided);
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QVERIFY(autofocus.unguided);
    Ekos::Manager::Instance()->focusModule()->mainFocuser()->abort();
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 5000);
    QVERIFY(!autofocus.unguided);

    KTELL("Run the autofocus to completion with guiding set to suspend\nGuiding required to suspend, then required to resume\nNo other autofocus started.");
    autofocus.started = autofocus.aborted = false;
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QVERIFY(autofocus.unguided);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.complete, 45000);
    QVERIFY(!autofocus.unguided);
    QVERIFY(!autofocus.started);

    KTELL("Abort the autofocus with guiding set to continue\nNo guiding signal emitted");
    focusSuspendGuiding->setCheckState(Qt::CheckState::Unchecked);
    autofocus.started = autofocus.aborted = autofocus.complete = autofocus.unguided = false;
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QVERIFY(!autofocus.unguided);
    Ekos::Manager::Instance()->focusModule()->mainFocuser()->abort();
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 5000);
    QVERIFY(!autofocus.unguided);

    KTELL("Run the autofocus to completion with guiding set to continue\nNo guiding signal emitted\nNo other autofocus started.");
    autofocus.started = autofocus.aborted = false;
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QVERIFY(!autofocus.unguided);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.complete, 45000);
    QVERIFY(!autofocus.unguided);
    QVERIFY(!autofocus.started);
}

void TestEkosFocus::testFocusWhenMountFlips()
{
    KTELL("Sync high on meridian to avoid jitter in CCD Simulator.\nConfigure a fast autofocus.");
    KTRY_FOCUS_SHOW();
    KTRY_MOUNT_SYNC(60.0, true, +10.0 / 3600);
    KTRY_FOCUS_MOVETO(35000);
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 5);
    KTRY_FOCUS_EXPOSURE(3, 99);
    KTRY_FOCUS_DETECT(3, 1, 99);
    if (focusDetectedStars() <= 0)
        QSKIP("CCD simulator reported no stars; meridian-flip focus assertions require detectable stars.");

    KTRY_FOCUS_GADGET(QPushButton, startFocusB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);

    // Prepare to detect the states of the autofocus_procedure
    KFocusProcedureSteps autofocus;
    QVERIFY(autofocus.starting);
    QVERIFY(autofocus.aborting);
    QVERIFY(autofocus.completing);

    KTELL("Ensure flip is enabled on meridian.\n.Start a standard autofocus.");
    Ekos::Manager::Instance()->mountModule()->setMeridianFlipValues(true, 0);
    QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->mountModule()->meridianFlipEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->mountModule()->meridianFlipValue() == 0, 1000);
    QVERIFY(!autofocus.started);
    QVERIFY(!autofocus.aborted);
    QVERIFY(!autofocus.complete);
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 5000);

    KTELL("Wait for the meridian flip to occur.\nCheck procedure aborts while flipping.");
    QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->mountModule()->status() == ISD::Mount::MOUNT_SLEWING, 15000);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 5000);

    KTELL("Wait for the flip to end.");
    QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->mountModule()->status() == ISD::Mount::MOUNT_TRACKING, 120000);

    KTELL("Start the procedure again.\nExpect the procedure to succeed.");
    autofocus.started = false;
    autofocus.aborted = false;
    autofocus.complete = false;
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.complete, 60000);
}

void TestEkosFocus::testFocusWhenHFRChecking()
{
    KTELL("Sync high on meridian to avoid jitter in CCD Simulator.\nConfigure a fast autofocus.");
    KTRY_FOCUS_SHOW();
    KTRY_MOUNT_SYNC(60.0, true, -1);
    int initialFocusPosition = 35000;
    KTRY_FOCUS_MOVETO(initialFocusPosition);
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 50);
    KTRY_FOCUS_EXPOSURE(3, 99);
    KTRY_FOCUS_DETECT(3, 1, 99);
    if (focusDetectedStars() <= 0)
        QSKIP("CCD simulator reported no stars; HFR-triggered autofocus assertions require detectable stars.");

    KTRY_FOCUS_GADGET(QPushButton, startFocusB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);

    // Prepare to detect the beginning of the autofocus_procedure
    KFocusProcedureSteps autofocus;
    QVERIFY(autofocus.starting);
    QVERIFY(autofocus.aborting);
    QVERIFY(autofocus.completing);

    KTELL("Run a standard autofocus.\nRun a HFR check.\nExpect no effect on the procedure.");
    QVERIFY(!autofocus.started);
    QVERIFY(!autofocus.aborted);
    QVERIFY(!autofocus.complete);
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 10000);

    KTELL("Wait a little, run a first HFR check while the procedure runs.");
    QTest::qWait(3000);
    Ekos::Manager::Instance()->focusModule()->mainFocuser()->checkFocus(0.1);

    KTELL("Expect procedure to succeed nonetheless.");
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.complete, 90000);

    KTELL("Run a second HFR check that would start an autofocus.");
    autofocus.complete = false;
    Ekos::Manager::Instance()->focusModule()->mainFocuser()->checkFocus(0.1);

    KTELL("Expect procedure to start properly.\nAbort the procedure manually.\nRun a third HFR check.");
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 10000);
    KTRY_FOCUS_CLICK(stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 10000);

    KTELL("Expect autofocus to start properly.\nChange settings so that the procedure fails now.\nExpect a failure.");
    autofocus.aborted = autofocus.complete = false;
    Ekos::Manager::Instance()->focusModule()->mainFocuser()->checkFocus(0.1);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 10000);
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 0.1, 0.1);
    KTRY_FOCUS_EXPOSURE(0.1, 1);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 90000);
    KTRY_FOCUS_CHECK_POSITION_WITH_TIMEOUT(initialFocusPosition, 5000);

    KTELL("Run a fourth HFR check.\nExpect autofocus to complete.");
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 50);
    KTRY_FOCUS_EXPOSURE(3, 99);
    autofocus.aborted = autofocus.complete = false;
    Ekos::Manager::Instance()->focusModule()->mainFocuser()->checkFocus(0.1);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 10000);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.complete, 90000);
}

void TestEkosFocus::testFocusFailure()
{
    KTELL("Sync high on meridian to avoid jitter in CCD Simulator");
    KTRY_FOCUS_SHOW();
    KTRY_MOUNT_SYNC(60.0, true, -1);

    KTELL("Configure an autofocus that cannot see any star, so that the initial setup fails.");
    KTRY_FOCUS_MOVETO(10000);
    KTRY_FOCUS_CONFIGURE("SEP", "Polynomial", 0.0, 1.0, 0.1);
    KTRY_FOCUS_EXPOSURE(0.01, 1);

    KTRY_FOCUS_GADGET(QPushButton, startFocusB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);

    // Prepare to detect the beginning of the autofocus_procedure
    KFocusProcedureSteps autofocus;
    QVERIFY(autofocus.starting);
    QVERIFY(autofocus.aborting);
    QVERIFY(autofocus.completing);

    KTELL("Run the autofocus, wait for the completion signal.\nExpect no further autofocus started as we are not running a sequence.");
    QVERIFY(!autofocus.started);
    QVERIFY(!autofocus.aborted);
    QVERIFY(!autofocus.complete);
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 30000);
    QVERIFY(!autofocus.started);

    QSKIP("Skipping abort test for device limits, focus algorithms are too sensitive to CCD Sim noise.");

    KTELL("Configure an autofocus that can see stars but is too far off and cannot achieve focus, so that the procedure fails.");
    KTRY_FOCUS_MOVETO(25000);
    QWARN("Iterative and Polynomial are too easily successful for this test.");
    KTRY_FOCUS_CONFIGURE("SEP", "Linear", 0.0, 100.0, 1.0);
    KTRY_FOCUS_EXPOSURE(5, 99);
    KTRY_FOCUS_GADGET(QDoubleSpinBox, maxTravelIN);
    maxTravelIN->setValue(2000);

    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);
    autofocus.started = false;
    autofocus.aborted = false;
    autofocus.complete = false;

    KTELL("Run the autofocus, wait for the completion signal.\nNo further autofocus started as we are not running a sequence.");
    QVERIFY(!autofocus.started);
    QVERIFY(!autofocus.aborted);
    QVERIFY(!autofocus.complete);
    KTRY_FOCUS_CLICK(startFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.started, 500);
    QTRY_VERIFY_WITH_TIMEOUT(autofocus.aborted, 240000);
    QVERIFY(!autofocus.started);
}

void TestEkosFocus::testStarDetection_data()
{
#if QT_VERSION < 0x050900
    QSKIP("Skipping fixture-based test on old QT version.");
#else
    QTest::addColumn<QString>("NAME");
    QTest::addColumn<QString>("RA");
    QTest::addColumn<QString>("DEC");

    // Altitude computation taken from SchedulerJob::findAltitude
    GeoLocation * const geo = KStarsData::Instance()->geo();
    KStarsDateTime const now(KStarsData::Instance()->lt());
    KSNumbers const numbers(now.djd());
    CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(now).gst());

    // std::list<char const *> Objects = { "Polaris", "Mizar", "M 51", "M 13", "M 47", "Vega", "NGC 2238", "M 81" };
    std::list<char const *> Objects = { "Polaris", "Mizar", "M 51", "M 47", "Vega", "M 81" };
    size_t count = 0;

    foreach (char const *name, Objects)
    {
        SkyObject const * const so = KStars::Instance()->data()->objectNamed(name);
        if (so != nullptr)
        {
            SkyObject o(*so);
            o.updateCoordsNow(&numbers);
            o.EquatorialToHorizontal(&LST, geo->lat());
            if (10.0 < o.alt().Degrees())
            {
                QTest::addRow("%s", name)
                        << name
                        << o.ra().toHMSString()
                        << o.dec().toDMSString();
                count++;
            }
            else QWARN(QString("Fixture '%1' altitude is '%2' degrees, discarding.").arg(name).arg(
                               so->alt().Degrees()).toStdString().c_str());
        }
    }

    if (!count)
        QSKIP("No usable fixture objects, bypassing test.");
#endif
}

void TestEkosFocus::testStarDetection()
{
#if QT_VERSION < 0x050900
    QSKIP("Skipping fixture-based test on old QT version.");
#else
    Ekos::Manager * const ekos = Ekos::Manager::Instance();
    QVERIFY(ekos);

    QFETCH(QString, NAME);
    QFETCH(QString, RA);
    QFETCH(QString, DEC);

    KTELL(QString(NAME + "\nSync to %1/%2 to make the mount teleport to the object.").arg(qPrintable(RA)).arg(qPrintable(DEC)));
    QTRY_VERIFY_WITH_TIMEOUT(ekos->mountModule() != nullptr, 5000);
    ekos->mountModule()->setMeridianFlipValues(false, 0);
    dms ra = dms::fromString(RA, false);
    dms de = dms::fromString(DEC, true);
    QVERIFY(ekos->mountModule()->sync(ra.Hours(), de.Degrees()));
    ekos->mountModule()->setTrackEnabled(true);

    KTELL(NAME + "\nWait for Focus to come up\nSwitch to Focus tab.");
    KTRY_FOCUS_SHOW();

    KTRY_FOCUS_GADGET(QPushButton, startFocusB);
    KTRY_FOCUS_GADGET(QPushButton, stopFocusB);
    QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000);
    QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000);

    KTRY_FOCUS_GADGET(QLabel, starsOut);

    KTELL(NAME + "\nMove focuser to see stars.");
    KTRY_FOCUS_MOVETO(35000);

    KTELL(NAME + "\nRun the detection with SEP.");
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0);
    KTRY_FOCUS_DETECT(1, 3, 99);
    if (focusDetectedStars() <= 0)
        QSKIP("CCD simulator reported no stars; fixture-based star-detection assertions require detectable stars.");
    QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000);

    KTELL(NAME + "\nRun the detection with Centroid.");
    KTRY_FOCUS_CONFIGURE("Centroid", "Iterative", 0.0, 100.0, 3.0);
    KTRY_FOCUS_DETECT(1, 3, 99);
    QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000);

    KTELL(NAME + "\nRun the detection with Threshold (no full-field).");
    KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0.0, 0.0, 3.0);
    KTRY_FOCUS_DETECT(1, 3, 99);
    QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000);

    KTELL(NAME + "\nRun the detection with Gradient (no full-field).");
    KTRY_FOCUS_CONFIGURE("Gradient", "Iterative", 0.0, 0.0, 3.0);
    KTRY_FOCUS_DETECT(1, 3, 99);
    QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000);

    KTELL(NAME + "\nRun the detection with SEP (8s capture).");
    KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0);
    KTRY_FOCUS_DETECT(8, 1, 99);
    QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000);

    KTELL(NAME + "\nRun the detection with SEP\nFull-field with various values\nHFR averaged on 3 frames.");
    for (double inner = 0.0; inner < 100.0; inner += 43.0)
    {
        for (double outer = 100.0; inner < outer; outer -= 42.0)
        {
            KTRY_FOCUS_CONFIGURE("SEP", "Iterative", inner, outer, 3.0);
            KTRY_FOCUS_DETECT(1, 2, 99);
        }
    }

    KTELL(NAME + "\nRun the detection with Threshold, full-field.");
    for (double threshold = 80.0; threshold < 99.0; threshold += 13.3)
    {
        KTRY_FOCUS_GADGET(QDoubleSpinBox, focusThreshold);
        focusThreshold->setValue(threshold);
        KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0, 0.0, 3.0);
        KTRY_FOCUS_DETECT(1, 1, 99);
    }
#endif
}

QTEST_KSTARS_MAIN(TestEkosFocus)

#endif // HAVE_INDI
