#include <doctest/doctest.h>

#include <map>

#include "torlinkc/engine/bootguard.hpp"
#include "torlinkc/engine/queue.hpp"
#include "test_support.hpp"

using namespace torlinkc;
using torlinkc::test::StateDirSandbox;

namespace {

// A 40-hex-char-valid id/magnet pair libtorrent's magnet parser accepts, so
// engine_.add() succeeds and these stay in the state the queue's own
// bookkeeping put them in, rather than flipping to "failed" on a parse error.
AddInput mk(int n) {
  char buf[41];
  std::snprintf(buf, sizeof(buf), "%040d", n);
  std::string id(buf);
  AddInput in;
  in.id = id;
  in.name = "T" + std::to_string(n);
  in.magnet = "magnet:?xt=urn:btih:" + id;
  return in;
}

std::map<std::string, DownloadStatus> statuses(const DownloadQueue& q) {
  std::map<std::string, DownloadStatus> out;
  for (const auto& it : q.getItems()) out[it.id] = it.status;
  return out;
}

QueueItem persistedItem(int n, DownloadStatus status, std::int64_t addedAt) {
  auto input = mk(n);
  QueueItem it;
  it.id = input.id;
  it.name = input.name;
  it.magnet = input.magnet;
  it.dir = "/d";
  it.status = status;
  it.addedAt = addedAt;
  return it;
}

}  // namespace

TEST_CASE("DownloadQueue concurrency cap: queues beyond the cap, promotes oldest queued on pause") {
  StateDirSandbox sandbox;
  DownloadQueue q(2);
  q.add(mk(1), "/d");
  q.add(mk(2), "/d");
  q.add(mk(3), "/d");

  CHECK(q.activeCount() == 2);
  auto s = statuses(q);
  CHECK(s[mk(1).id] == DownloadStatus::Downloading);
  CHECK(s[mk(2).id] == DownloadStatus::Downloading);
  CHECK(s[mk(3).id] == DownloadStatus::Queued);

  // Pausing an active download frees a slot -> the queued one starts.
  q.pause(mk(1).id);
  CHECK(q.activeCount() == 2);
  s = statuses(q);
  CHECK(s[mk(1).id] == DownloadStatus::Paused);
  CHECK(s[mk(3).id] == DownloadStatus::Downloading);
}

TEST_CASE("DownloadQueue defaults to unlimited (maxDownloads 0): everything downloads at once") {
  StateDirSandbox sandbox;
  DownloadQueue q(0);
  q.add(mk(1), "/d");
  q.add(mk(2), "/d");
  q.add(mk(3), "/d");
  CHECK(q.activeCount() == 3);
  for (const auto& [id, status] : statuses(q)) CHECK(status == DownloadStatus::Downloading);
}

TEST_CASE("DownloadQueue respects the cap when restoring persisted downloads on boot") {
  StateDirSandbox sandbox;
  std::vector<QueueItem> persisted = {
      persistedItem(1, DownloadStatus::Downloading, 1),
      persistedItem(2, DownloadStatus::Downloading, 2),
      persistedItem(3, DownloadStatus::Downloading, 3),
  };
  DownloadQueue q(2);
  q.restore(persisted);
  CHECK(q.activeCount() == 2);
  CHECK(statuses(q)[mk(3).id] == DownloadStatus::Queued);
}

TEST_CASE("DownloadQueue starts persisted queued items on restore when the cap is unset") {
  StateDirSandbox sandbox;
  std::vector<QueueItem> persisted = {
      persistedItem(1, DownloadStatus::Downloading, 1),
      persistedItem(2, DownloadStatus::Queued, 2),
      persistedItem(3, DownloadStatus::Queued, 3),
  };
  DownloadQueue q(0);
  q.restore(persisted);
  CHECK(q.activeCount() == 3);
  for (const auto& [id, status] : statuses(q)) CHECK(status == DownloadStatus::Downloading);
}

TEST_CASE("DownloadQueue safe-mode restore brings active and queued items back paused") {
  StateDirSandbox sandbox;
  DownloadQueue q;
  std::vector<QueueItem> persisted = {
      persistedItem(1, DownloadStatus::Downloading, 1),
      persistedItem(2, DownloadStatus::Queued, 2),
      persistedItem(3, DownloadStatus::Failed, 3),
      persistedItem(4, DownloadStatus::Paused, 4),
  };
  q.restore(persisted, RestoreOptions{true});

  auto s = statuses(q);
  CHECK(s[mk(1).id] == DownloadStatus::Paused);
  CHECK(s[mk(2).id] == DownloadStatus::Paused);
  CHECK(s[mk(3).id] == DownloadStatus::Failed);
  CHECK(s[mk(4).id] == DownloadStatus::Paused);
}

TEST_CASE("DownloadQueue safe-mode restoreSeeds brings persisted seeders back paused") {
  StateDirSandbox sandbox;
  DownloadQueue q;
  HistoryItem h1;
  h1.id = "s1";
  h1.name = "Seed One";
  h1.magnet = "magnet:?xt=urn:btih:" + std::string(40, '1');
  h1.dir = "/d";
  HistoryItem h2 = h1;
  h2.id = "s2";
  h2.name = "Seed Two";
  h2.magnet = "magnet:?xt=urn:btih:" + std::string(40, '2');

  q.restoreHistory({h1, h2});
  q.restoreSeeds({SeedRecord{"s1", SeedStatus::Seeding}, SeedRecord{"s2", SeedStatus::Paused}}, RestoreOptions{true});

  REQUIRE(q.getSeed("s1").has_value());
  REQUIRE(q.getSeed("s2").has_value());
  CHECK(q.getSeed("s1")->status == SeedStatus::Paused);
  CHECK(q.getSeed("s2")->status == SeedStatus::Paused);
  CHECK(q.seedingCount() == 0);
}

TEST_CASE("DownloadQueue::add on an existing non-failed item is a no-op") {
  StateDirSandbox sandbox;
  DownloadQueue q;
  auto input = mk(1);
  q.add(input, "/d");
  q.pause(input.id);
  REQUIRE(statuses(q)[input.id] == DownloadStatus::Paused);

  // Re-adding a paused (not failed) item must not restart it.
  q.add(input, "/d");
  CHECK(statuses(q)[input.id] == DownloadStatus::Paused);
}

TEST_CASE("DownloadQueue::cancel removes an active download entirely") {
  StateDirSandbox sandbox;
  DownloadQueue q;
  auto input = mk(1);
  q.add(input, "/d");
  REQUIRE(q.has(input.id));
  q.cancel(input.id);
  CHECK_FALSE(q.has(input.id));
}

TEST_CASE("DownloadQueue::persistSync disarms the boot marker") {
  StateDirSandbox sandbox;
  armBootMarker();
  REQUIRE(wasBootInterrupted());
  DownloadQueue q;
  q.persistSync();
  CHECK_FALSE(wasBootInterrupted());
}
