#include <gtest/gtest.h>

#include <cstring>
#include <vector>

#define WITH_MQTT_BRIDGE 1
#include "helpers/MQTTPrefsCodec.h"

namespace Codec = MQTTPrefsCodec;

namespace {

MQTTPrefs defaults() {
  MQTTPrefs prefs = {};
  prefs.mqtt_status_enabled = 1;
  prefs.mqtt_packets_enabled = 1;
  prefs.mqtt_tx_enabled = 2;
  prefs.mqtt_rx_enabled = 1;
  prefs.mqtt_status_interval = 300000;
  prefs.wifi_power_save = 1;
  for (int i = 0; i < MQTT_PREFS_SLOT_COUNT; ++i) {
    strncpy(prefs.mqtt_slot_preset[i], "none", sizeof(prefs.mqtt_slot_preset[i]) - 1);
  }
  strncpy(prefs.snmp_community, "public", sizeof(prefs.snmp_community) - 1);
  prefs.radio_watchdog_minutes = 5;
  prefs.alert_wifi_minutes = 30;
  prefs.alert_mqtt_minutes = 240;
  prefs.alert_min_interval_min = 60;
  return prefs;
}

void writeText(std::vector<uint8_t>* bytes, size_t offset, const char* value) {
  const size_t length = strlen(value);
  ASSERT_LE(offset + length, bytes->size());
  memcpy(bytes->data() + offset, value, length);
}

void writeLe16(std::vector<uint8_t>* bytes, size_t offset, uint16_t value) {
  ASSERT_LE(offset + 2, bytes->size());
  (*bytes)[offset] = static_cast<uint8_t>(value & 0xff);
  (*bytes)[offset + 1] = static_cast<uint8_t>(value >> 8);
}

void writeHeader(std::vector<uint8_t>* bytes, uint16_t version, uint16_t payload_len) {
  ASSERT_GE(bytes->size(), sizeof(MQTTPrefsHeader));
  (*bytes)[0] = MQTT_PREFS_MAGIC[0];
  (*bytes)[1] = MQTT_PREFS_MAGIC[1];
  (*bytes)[2] = MQTT_PREFS_MAGIC[2];
  (*bytes)[3] = MQTT_PREFS_MAGIC[3];
  writeLe16(bytes, 4, version);
  writeLe16(bytes, 6, payload_len);
}

Codec::DecodePlan classify(const std::vector<uint8_t>& bytes) {
  const size_t prefix_size = bytes.size() < sizeof(MQTTPrefsHeader)
      ? bytes.size() : sizeof(MQTTPrefsHeader);
  return Codec::classify(bytes.data(), prefix_size, bytes.size());
}

void fillHighEntropy(std::vector<uint8_t>* bytes) {
  uint32_t state = 0x89abcdef;
  for (size_t i = 0; i < bytes->size(); ++i) {
    state = state * 1664525u + 1013904223u;
    // Keep every byte non-NUL so this is a useful corruption fixture rather
    // than a sparse/default-like legacy file.
    (*bytes)[i] = static_cast<uint8_t>((state >> 24) | 0x80);
  }
}

}  // namespace

TEST(MQTTPrefsCodec, MigratesPostWifiPowerPreSlotFixture) {
  // Frozen post-WiFi-power pre-slot offsets: status=44, ssid=48, power=144,
  // timezone=145, server=178, port=242. Do not derive this fixture from structs.
  std::vector<uint8_t> bytes(472, 0);
  writeText(&bytes, 0, "legacy-node");
  writeText(&bytes, 32, "YYZ");
  bytes[40] = 1;
  bytes[41] = 1;
  bytes[43] = 2;
  bytes[144] = 2;
  writeText(&bytes, 145, "UTC");
  writeText(&bytes, 178, "broker.example");
  writeLe16(&bytes, 242, 1883);
  writeText(&bytes, 244, "alice");
  writeText(&bytes, 276, "secret");
  bytes[340] = 1;

  const Codec::DecodePlan plan = classify(bytes);
  ASSERT_EQ(Codec::Source::LegacyPreSlot, plan.source);
  ASSERT_TRUE(plan.rewrite_legacy);
  ASSERT_FALSE(Codec::looksLikePreWifiPower(bytes.data(), bytes.size()));
  ASSERT_TRUE(Codec::isPlausibleLegacy(plan.source, bytes.data(), bytes.size()));

  OldMQTTPrefs old_prefs = {};
  memcpy(&old_prefs, bytes.data(), sizeof(old_prefs));
  MQTTPrefs prefs = defaults();
  Codec::migratePreSlot(old_prefs, &prefs);

  EXPECT_STREQ("legacy-node", prefs.mqtt_origin);
  EXPECT_STREQ("analyzer-us", prefs.mqtt_slot_preset[0]);
  EXPECT_STREQ("custom", prefs.mqtt_slot_preset[2]);
  EXPECT_STREQ("broker.example", prefs.mqtt_slot_host[2]);
  EXPECT_EQ(1883, prefs.mqtt_slot_port[2]);
  EXPECT_STREQ("secret", prefs.mqtt_slot_password[2]);
}

TEST(MQTTPrefsCodec, MigratesPreWifiPowerFixtureWithConservativeHeuristic) {
  // In the pre-WiFi-power variant timezone starts at 144 and server at 177;
  // port still aligns at 242. A non-empty timezone makes the layout unambiguous.
  std::vector<uint8_t> bytes(472, 0);
  writeText(&bytes, 0, "pre-power");
  writeText(&bytes, 144, "PST8PDT");
  writeText(&bytes, 177, "old-broker");
  writeLe16(&bytes, 242, 8883);

  ASSERT_TRUE(Codec::looksLikePreWifiPower(bytes.data(), bytes.size()));
  ASSERT_TRUE(Codec::isPlausibleLegacy(Codec::Source::LegacyPreSlot,
                                       bytes.data(), bytes.size()));
  PreWifiPowerOldMQTTPrefs old_prefs = {};
  memcpy(&old_prefs, bytes.data(), sizeof(old_prefs));
  MQTTPrefs prefs = defaults();
  Codec::migratePreWifiPower(old_prefs, &prefs);

  EXPECT_STREQ("PST8PDT", prefs.timezone_string);
  EXPECT_STREQ("old-broker", prefs.mqtt_slot_host[2]);
  EXPECT_EQ(8883, prefs.mqtt_slot_port[2]);
  EXPECT_EQ(1, prefs.wifi_power_save);  // retained current default; old layout had none
}

TEST(MQTTPrefsCodec, DetectsPreWifiPowerFixtureWithOnlyUtcOffsetConfigured) {
  std::vector<uint8_t> bytes(472, 0);
  bytes[176] = static_cast<uint8_t>(-8);

  EXPECT_TRUE(Codec::looksLikePreWifiPower(bytes.data(), bytes.size()));
}

TEST(MQTTPrefsCodec, MigratesThreeSlotBaseAndExtendedFixtures) {
  // Frozen 3-slot offsets: presets=178, hosts=250, ports=442, users=448,
  // passwords=544, owner=736, tokens=1030, topics=1174. The base file has
  // two tail-padding bytes, so its frozen size is 1032.
  std::vector<uint8_t> base(1032, 0);
  writeText(&base, 0, "three-base");
  writeText(&base, 178, "meshmapper");
  writeText(&base, 250, "three.example");
  writeLe16(&base, 442, 1884);
  const Codec::DecodePlan base_plan = classify(base);
  ASSERT_EQ(Codec::Source::LegacyThreeSlotBase, base_plan.source);
  ASSERT_TRUE(Codec::isPlausibleLegacy(base_plan.source, base.data(), base.size()));
  ThreeSlotBaseMQTTPrefs old_base = {};
  memcpy(&old_base, base.data(), sizeof(old_base));
  MQTTPrefs base_prefs = defaults();
  Codec::migrateThreeSlot(old_base, &base_prefs);
  EXPECT_STREQ("three-base", base_prefs.mqtt_origin);
  EXPECT_STREQ("meshmapper", base_prefs.mqtt_slot_preset[0]);
  EXPECT_EQ(1884, base_prefs.mqtt_slot_port[0]);
  EXPECT_STREQ("none", base_prefs.mqtt_slot_preset[3]);

  std::vector<uint8_t> extended(1464, 0);
  writeText(&extended, 0, "three-extended");
  writeText(&extended, 178 + 24, "custom");
  writeText(&extended, 250 + 64, "slot-two.example");
  writeText(&extended, 1030 + 48, "token-two");
  writeText(&extended, 1174 + 96, "custom/{type}");
  const Codec::DecodePlan extended_plan = classify(extended);
  ASSERT_EQ(Codec::Source::LegacyThreeSlot, extended_plan.source);
  ASSERT_TRUE(Codec::isPlausibleLegacy(extended_plan.source, extended.data(), extended.size()));
  ThreeSlotMQTTPrefs old_extended = {};
  memcpy(&old_extended, extended.data(), sizeof(old_extended));
  MQTTPrefs extended_prefs = defaults();
  Codec::migrateThreeSlot(old_extended, &extended_prefs);
  EXPECT_STREQ("custom", extended_prefs.mqtt_slot_preset[1]);
  EXPECT_STREQ("token-two", extended_prefs.mqtt_slot_token[1]);
  EXPECT_STREQ("custom/{type}", extended_prefs.mqtt_slot_topic[1]);
}

TEST(MQTTPrefsCodec, MigratesAllLegacySixSlotPrefixesWithoutClobberingDefaults) {
  // Frozen 6-slot offsets: presets=178, hosts=322, ports=706, tokens=1588,
  // topics=1876, audience=2452, rx=2836, ntp=2837.
  for (const size_t size : {size_t(2452), size_t(2836), size_t(2840), size_t(2904)}) {
    std::vector<uint8_t> bytes(size, 0);
    writeText(&bytes, 0, "six-slot");
    writeText(&bytes, 178 + 5 * 24, "custom");
    writeText(&bytes, 322 + 5 * 64, "six.example");
    writeText(&bytes, 1588 + 5 * 48, "token-six");
    writeText(&bytes, 1876 + 5 * 96, "six/{type}");
    if (size >= 2836) writeText(&bytes, 2452 + 5 * 64, "audience-six");
    if (size >= 2840) bytes[2836] = 0;
    if (size >= 2904) writeText(&bytes, 2837, "time.example");

    const Codec::DecodePlan plan = classify(bytes);
    const Codec::Source expected_source = size == 2452 ? Codec::Source::LegacySixSlotBase
        : size == 2836 ? Codec::Source::LegacySixSlotAudience
        : size == 2840 ? Codec::Source::LegacySixSlotAudienceRx
        : Codec::Source::LegacySixSlot;
    EXPECT_EQ(expected_source, plan.source) << size;
    ASSERT_TRUE(plan.rewrite_legacy);
    ASSERT_TRUE(Codec::isPlausibleLegacy(plan.source, bytes.data(), bytes.size())) << size;
    Legacy6SlotMQTTPrefs old_prefs = {};
    memcpy(&old_prefs, bytes.data(), bytes.size());
    MQTTPrefs prefs = defaults();
    Codec::migrateLegacySixSlot(old_prefs, plan.source, &prefs);

    EXPECT_STREQ("custom", prefs.mqtt_slot_preset[5]);
    EXPECT_STREQ("token-six", prefs.mqtt_slot_token[5]);
    if (size < 2836) {
      EXPECT_EQ('\0', prefs.mqtt_slot_audience[5][0]);
    } else {
      EXPECT_STREQ("audience-six", prefs.mqtt_slot_audience[5]);
    }
    if (size < 2840) {
      EXPECT_EQ(1, prefs.mqtt_rx_enabled) << size;
    } else {
      EXPECT_EQ(0, prefs.mqtt_rx_enabled);
    }
    if (size < 2904) {
      EXPECT_EQ('\0', prefs.mqtt_ntp_server[0]);
    } else {
      EXPECT_STREQ("time.example", prefs.mqtt_ntp_server);
    }
  }
}

TEST(MQTTPrefsCodec, CurrentVersionedPayloadRoundTripsExactly) {
  MQTTPrefs source = defaults();
  strncpy(source.mqtt_origin, "current-node", sizeof(source.mqtt_origin) - 1);
  strncpy(source.mqtt_slot_password[2], "preserve-me", sizeof(source.mqtt_slot_password[2]) - 1);
  strncpy(source.alert_region, "PNW", sizeof(source.alert_region) - 1);
  source.mqtt_neighbors_enabled = 1;
  source.mqtt_neighbors_interval = MQTT_NEIGHBORS_MAX_INTERVAL_MS;
  std::vector<uint8_t> bytes(Codec::kEncodedSize);
  ASSERT_EQ(Codec::kEncodedSize, Codec::encode(source, bytes.data(), bytes.size()));

  const Codec::DecodePlan plan = classify(bytes);
  ASSERT_EQ(Codec::Source::Current, plan.source);
  ASSERT_FALSE(plan.preserve_file);
  ASSERT_TRUE(plan.observer_fields_present);
  MQTTPrefs loaded = defaults();
  memcpy(&loaded, bytes.data() + sizeof(MQTTPrefsHeader), plan.payload_len);
  EXPECT_EQ(0, memcmp(&source, &loaded, sizeof(source)));
}

TEST(MQTTPrefsCodec, CompatibleShortV1PayloadPreservesDefaultsBeyondObserverBoundary) {
  MQTTPrefs source = defaults();
  strncpy(source.mqtt_origin, "short-v1-node", sizeof(source.mqtt_origin) - 1);
  strncpy(source.mqtt_ntp_server, "ntp.short.example", sizeof(source.mqtt_ntp_server) - 1);
  source.snmp_enabled = 1;
  strncpy(source.snmp_community, "do-not-copy", sizeof(source.snmp_community) - 1);
  source.radio_watchdog_minutes = 99;

  std::vector<uint8_t> bytes(sizeof(MQTTPrefsHeader) + Codec::kV1PreObserverPayloadSize, 0);
  writeHeader(&bytes, MQTT_PREFS_VERSION,
              static_cast<uint16_t>(Codec::kV1PreObserverPayloadSize));
  memcpy(bytes.data() + sizeof(MQTTPrefsHeader), &source, Codec::kV1PreObserverPayloadSize);

  const Codec::DecodePlan plan = classify(bytes);
  ASSERT_EQ(Codec::Source::Current, plan.source);
  ASSERT_EQ(Codec::kV1PreObserverPayloadSize, plan.payload_len);
  ASSERT_FALSE(plan.preserve_file);
  ASSERT_FALSE(plan.observer_fields_present);

  MQTTPrefs loaded = defaults();
  memcpy(&loaded, bytes.data() + sizeof(MQTTPrefsHeader), plan.payload_len);
  EXPECT_STREQ("short-v1-node", loaded.mqtt_origin);
  EXPECT_STREQ("ntp.short.example", loaded.mqtt_ntp_server);
  EXPECT_EQ(0, loaded.snmp_enabled);
  EXPECT_STREQ("public", loaded.snmp_community);
  EXPECT_EQ(5, loaded.radio_watchdog_minutes);
}

TEST(MQTTPrefsCodec, PreNeighborsV1PayloadLoadsObserverFieldsAndDefaultsNeighborsTail) {
  // A /mqtt_prefs written by observer/webconfig firmware before the neighbors
  // tail existed: full observer fields, 2860-byte v1 payload. It must still load
  // as Current (observer fields present) with the neighbors tail defaulted.
  MQTTPrefs source = defaults();
  strncpy(source.mqtt_origin, "pre-neighbors-node", sizeof(source.mqtt_origin) - 1);
  strncpy(source.alert_region, "PNW", sizeof(source.alert_region) - 1);
  source.snmp_enabled = 1;
  source.alert_enabled = 1;
  source.mqtt_neighbors_enabled = 0;            // old struct's byte 2857 was zero padding
  source.mqtt_neighbors_interval = 0x11223344;  // must NOT survive a 2860-byte read

  std::vector<uint8_t> bytes(sizeof(MQTTPrefsHeader) + Codec::kV1PreNeighborsPayloadSize, 0);
  writeHeader(&bytes, MQTT_PREFS_VERSION,
              static_cast<uint16_t>(Codec::kV1PreNeighborsPayloadSize));
  memcpy(bytes.data() + sizeof(MQTTPrefsHeader), &source, Codec::kV1PreNeighborsPayloadSize);

  const Codec::DecodePlan plan = classify(bytes);
  ASSERT_EQ(Codec::Source::Current, plan.source);
  ASSERT_EQ(Codec::kV1PreNeighborsPayloadSize, plan.payload_len);
  ASSERT_FALSE(plan.preserve_file);
  ASSERT_TRUE(plan.observer_fields_present);

  MQTTPrefs loaded = defaults();
  loaded.mqtt_neighbors_enabled = 1;                                   // pretend stale
  loaded.mqtt_neighbors_interval = MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS;  // caller's defaulted tail
  memcpy(&loaded, bytes.data() + sizeof(MQTTPrefsHeader), plan.payload_len);

  EXPECT_STREQ("pre-neighbors-node", loaded.mqtt_origin);
  EXPECT_STREQ("PNW", loaded.alert_region);
  EXPECT_EQ(1, loaded.snmp_enabled);
  EXPECT_EQ(1, loaded.alert_enabled);
  // Enable flag sits at offset 2857 (inside the 2860 read) -> takes the file's 0.
  // Interval begins at 2860 (beyond the read) -> keeps the caller's default.
  EXPECT_EQ(0u, loaded.mqtt_neighbors_enabled);
  EXPECT_EQ(MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS, loaded.mqtt_neighbors_interval);
}

TEST(MQTTPrefsCodec, CorruptOrShortVersionedInputsArePreserved) {
  Codec::DecodePlan plan = Codec::classify(nullptr, 0, 0);
  EXPECT_EQ(Codec::Source::Corrupt, plan.source);
  EXPECT_TRUE(plan.preserve_file);

  std::vector<uint8_t> partial_magic = {MQTT_PREFS_MAGIC[0], MQTT_PREFS_MAGIC[1], MQTT_PREFS_MAGIC[2]};
  plan = classify(partial_magic);
  EXPECT_EQ(Codec::Source::Corrupt, plan.source);
  EXPECT_TRUE(plan.preserve_file);

  std::vector<uint8_t> short_payload(sizeof(MQTTPrefsHeader) + 4, 0);
  writeHeader(&short_payload, MQTT_PREFS_VERSION,
              static_cast<uint16_t>(Codec::kV1BaselinePayloadSize - 1));
  plan = classify(short_payload);
  EXPECT_EQ(Codec::Source::Corrupt, plan.source);
  EXPECT_TRUE(plan.preserve_file);

  std::vector<uint8_t> declared_short_with_full_body(
      sizeof(MQTTPrefsHeader) + Codec::kV1BaselinePayloadSize, 0);
  writeHeader(&declared_short_with_full_body, MQTT_PREFS_VERSION,
              static_cast<uint16_t>(Codec::kV1PreObserverPayloadSize));
  plan = classify(declared_short_with_full_body);
  EXPECT_EQ(Codec::Source::Corrupt, plan.source);
  EXPECT_TRUE(plan.preserve_file);

  std::vector<uint8_t> declared_full_with_short_body(
      sizeof(MQTTPrefsHeader) + Codec::kV1PreObserverPayloadSize, 0);
  writeHeader(&declared_full_with_short_body, MQTT_PREFS_VERSION,
              static_cast<uint16_t>(Codec::kV1BaselinePayloadSize));
  plan = classify(declared_full_with_short_body);
  EXPECT_EQ(Codec::Source::Corrupt, plan.source);
  EXPECT_TRUE(plan.preserve_file);

  std::vector<uint8_t> truncated(sizeof(MQTTPrefsHeader) + Codec::kV1BaselinePayloadSize - 1, 0);
  writeHeader(&truncated, MQTT_PREFS_VERSION,
              static_cast<uint16_t>(Codec::kV1BaselinePayloadSize));
  plan = classify(truncated);
  EXPECT_EQ(Codec::Source::Corrupt, plan.source);
  EXPECT_TRUE(plan.preserve_file);

  std::vector<uint8_t> trailing(sizeof(MQTTPrefsHeader) + Codec::kV1BaselinePayloadSize + 1, 0);
  writeHeader(&trailing, MQTT_PREFS_VERSION,
              static_cast<uint16_t>(Codec::kV1BaselinePayloadSize));
  plan = classify(trailing);
  EXPECT_EQ(Codec::Source::Corrupt, plan.source);
  EXPECT_TRUE(plan.preserve_file);
}

TEST(MQTTPrefsCodec, LegacyPlausibilityRejectsHighEntropyBytesAtEveryWhitelistedSize) {
  // A headerless raw struct has no checksum, so this only reduces false
  // migrations; it cannot prove that a plausible-looking file is authentic.
  for (const size_t size : {size_t(472), size_t(1032), size_t(1464), size_t(2452),
                            size_t(2836), size_t(2840), size_t(2904)}) {
    std::vector<uint8_t> bytes(size, 0);
    fillHighEntropy(&bytes);
    const Codec::DecodePlan plan = classify(bytes);
    ASSERT_TRUE(plan.rewrite_legacy) << size;
    EXPECT_FALSE(Codec::isPlausibleLegacy(plan.source, bytes.data(), bytes.size())) << size;
  }
}

TEST(MQTTPrefsCodec, UnsupportedHeaderlessSizesArePreserved) {
  // 3024 was produced briefly before versioning, but repository history says
  // that raw observer-tail form was not shipped. Preserve it rather than guess.
  for (const size_t size : {size_t(471), size_t(473), size_t(1465), size_t(2905), size_t(3024)}) {
    std::vector<uint8_t> bytes(size, 0);
    const Codec::DecodePlan plan = classify(bytes);
    EXPECT_EQ(Codec::Source::Corrupt, plan.source) << size;
    EXPECT_TRUE(plan.preserve_file) << size;
  }
}

TEST(MQTTPrefsCodec, NewerAndSameVersionExtendedPayloadsAreHeld) {
  std::vector<uint8_t> newer(sizeof(MQTTPrefsHeader), 0);
  writeHeader(&newer, MQTT_PREFS_VERSION + 1, 0);
  Codec::DecodePlan plan = classify(newer);
  EXPECT_EQ(Codec::Source::UnsupportedVersion, plan.source);
  EXPECT_TRUE(plan.preserve_file);

  std::vector<uint8_t> extended(sizeof(MQTTPrefsHeader) + Codec::kV1BaselinePayloadSize + 1, 0);
  writeHeader(&extended, MQTT_PREFS_VERSION,
              static_cast<uint16_t>(Codec::kV1BaselinePayloadSize + 1));
  plan = classify(extended);
  EXPECT_EQ(Codec::Source::UnsupportedVersion, plan.source);
  EXPECT_TRUE(plan.preserve_file);
}

int main(int argc, char** argv) {
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}
