// Sane Break is a gentle break reminder that helps you avoid mindlessly skipping breaks
// Copyright (C) 2024-2026 Sane Break developers
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once
#include <gmock/gmock.h>
#include <qtestcase.h>

#include <QObject>
#include <QSettings>
#include <QSqlDatabase>
#include <QTemporaryFile>
#include <QTest>
#include <memory>

#include "core/app-states.h"
#include "core/app.h"
#include "core/break-windows.h"
#include "core/db.h"
#include "core/flags.h"
#include "core/idle-time.h"
#include "core/meeting-prompt.h"
#include "core/preferences.h"
#include "core/system-monitor.h"
#include "core/timer.h"
#include "gmock/gmock.h"

inline SanePreferences* tempPreferences() {
  QTemporaryFile tempFile;
  (void)tempFile.open();
  return new SanePreferences(new QSettings(tempFile.fileName(), QSettings::IniFormat));
}

class DummyIdleTime : public SystemIdleTime {
  Q_OBJECT
 public:
  using SystemIdleTime::SystemIdleTime;
  void startWatching() override { m_isIdle = false; };
  void stopWatching() override {};
  void setWatchAccuracy(int) override {};
  void setIdleDetection(int idleTime, IdleMode mode) override {
    // Keep the base members in sync so minIdleTime()/idleMode() reflect what the
    // production backends would have applied (the app reads them back, e.g. the
    // treatInhibitorAsActivity preference handler).
    SystemIdleTime::setIdleDetection(idleTime, mode);
    m_recordedMinIdleTime = idleTime;
    m_recordedMode = mode;
    // Some backends (InhibitorProxyIdleTime over Wayland) can emit idleStart
    // synchronously from within setIdleDetection, e.g. when a break switches to
    // InputOnly while the user is idle. One-shot flag to simulate that.
    if (m_emitIdleStartOnDetection) {
      m_emitIdleStartOnDetection = false;
      setIdle(true);
    }
  };
  IdleMode recordedMode() const { return m_recordedMode; };
  int recordedMinIdleTime() const { return m_recordedMinIdleTime; };
  void setEmitIdleStartOnDetection(bool enable) { m_emitIdleStartOnDetection = enable; }
  bool isInhibited() const override { return m_inhibited; }
  void setInhibited(bool inhibited) { m_inhibited = inhibited; }
  void setIdle(bool idle) {
    m_isIdle = idle;
    if (idle) {
      emit idleStart();
    } else {
      emit idleEnd();
    }
  }

 private:
  IdleMode m_recordedMode = IdleMode::InhibitorAware;
  int m_recordedMinIdleTime = 0;
  bool m_emitIdleStartOnDetection = false;
  bool m_inhibited = false;
};

class DummyBreakWindows : public AbstractBreakWindows {
  Q_OBJECT
 public:
  using AbstractBreakWindows::AbstractBreakWindows;
  MOCK_METHOD(void, create, (BreakType, SanePreferences*, int, bool), (override));
  MOCK_METHOD(void, destroy, (), ());
  MOCK_METHOD(void, setTime, (int), ());
  MOCK_METHOD(void, showFullScreen, (), ());
  MOCK_METHOD(void, showFlashPrompt, (), ());
  MOCK_METHOD(void, showButtons, (Buttons, bool), ());
  MOCK_METHOD(void, playEnterSound, (BreakType, SanePreferences*), ());
  MOCK_METHOD(void, playExitSound, (BreakType, SanePreferences*), ());
  MOCK_METHOD(void, showHeadsUp, (int, BreakType, SanePreferences*), ());
  MOCK_METHOD(void, setHeadsUpTime, (int), ());
  MOCK_METHOD(void, hideHeadsUp, (), ());
};

class DummySystemMonitor : public AbstractSystemMonitor {
  Q_OBJECT
 public:
  using AbstractSystemMonitor::AbstractSystemMonitor;

  void start() {};
  bool isOnBattery() { return m_battery; };
  void setOnBattery(bool battery) { m_battery = battery; };

 private:
  bool m_battery = false;
};

class DummyMeetingPrompt : public AbstractMeetingPrompt {
  Q_OBJECT
 public:
  using AbstractMeetingPrompt::AbstractMeetingPrompt;
  MOCK_METHOD(void, showEndPrompt, (), (override));
  MOCK_METHOD(void, closeEndPrompt, (), (override));
  MOCK_METHOD(void, setTime, (int seconds), (override));
};

struct DummyAppDependencies {
  SanePreferences* preferences = nullptr;
  BreakDatabase* db = nullptr;
  AbstractTimer* countDownTimer = nullptr;
  AbstractTimer* screenLockTimer = nullptr;
  DummyIdleTime* idleTimer = nullptr;
  DummySystemMonitor* systemMonitor = nullptr;
  DummyBreakWindows* breakWindows = nullptr;
  DummyMeetingPrompt* meetingPrompt = nullptr;
};
class DummyApp : public AbstractApp {
  Q_OBJECT
 public:
  DummyApp(const DummyAppDependencies& deps, QObject* parent = nullptr)
      : AbstractApp(
            {deps.preferences, deps.db, deps.countDownTimer, deps.screenLockTimer,
             deps.idleTimer, deps.systemMonitor, deps.breakWindows, deps.meetingPrompt},
            parent) {
    connect(this, &DummyApp::trayDataUpdated, this,
            [this](TrayData data) { trayData = data; });
  };
  MOCK_METHOD(void, doLockScreen, (), (override));
  AppState::StateID currentState() { return m_currentState->getID(); }
  void advance(int secs) {
    QVERIFY2(m_countDownTimer->isActive(),
             "Impossible to simulate timer tick with an inactive timer");
    for (int i = 0; i < secs; i++) tick();
  }
  void advanceToBreakEnd() {
    while (m_currentState->getID() == AppState::Break) tick();
  }
  void advanceToForceBreakStart() {
    QCOMPARE(m_currentState->getID(), AppState::Break);
    auto* breakState = dynamic_cast<AppStateBreak*>(m_currentState.get());
    while (!breakState->data->isForceBreak()) tick();
  }
  TrayData trayData;
  static DummyAppDependencies makeDeps(QObject* parent = nullptr) {
    for (auto& conn : QSqlDatabase::connectionNames()) {
      QSqlDatabase::removeDatabase(conn);
    }
    auto db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(":memory:");
    auto preferences = tempPreferences();
    return {
        .preferences = preferences,
        .db = new BreakDatabase(db, parent),
        .countDownTimer = new AbstractTimer(parent),
        .screenLockTimer = new AbstractTimer(parent),
        .idleTimer = new DummyIdleTime(parent),
        .systemMonitor = new DummySystemMonitor(parent),
        .breakWindows = new testing::NiceMock<DummyBreakWindows>(parent),
        .meetingPrompt = new testing::NiceMock<DummyMeetingPrompt>(parent, preferences),
    };
  };
};
