#include <etl/crc32.h>

#include <catch2/catch_test_macros.hpp>
#include <filesystem>
#include <random>

#include "fsfw/cfdp.h"
#include "fsfw/cfdp/handler/PutRequest.h"
#include "fsfw/cfdp/handler/SourceHandler.h"
#include "fsfw/cfdp/pdu/EofPduCreator.h"
#include "fsfw/cfdp/pdu/EofPduReader.h"
#include "fsfw/cfdp/pdu/FileDataReader.h"
#include "fsfw/cfdp/pdu/MetadataPduCreator.h"
#include "fsfw/cfdp/pdu/MetadataPduReader.h"
#include "fsfw/tmtcservices/TmTcMessage.h"
#include "fsfw/util/SeqCountProvider.h"
#include "mocks/AcceptsTmMock.h"
#include "mocks/EventReportingProxyMock.h"
#include "mocks/FilesystemMock.h"
#include "mocks/MessageQueueMock.h"
#include "mocks/StorageManagerMock.h"
#include "mocks/cfdp/FaultHandlerMock.h"
#include "mocks/cfdp/RemoteConfigTableMock.h"
#include "mocks/cfdp/UserMock.h"

TEST_CASE("CFDP Source Handler", "[cfdp]") {
  using namespace cfdp;
  using namespace returnvalue;
  using namespace std::filesystem;
  const size_t MAX_FILE_SEGMENT_SIZE = 255;

  MessageQueueId_t destQueueId = 2;
  AcceptsTmMock tmReceiver(destQueueId);
  MessageQueueMock mqMock(destQueueId);
  EntityId localId = EntityId(UnsignedByteField<uint16_t>(2));
  EntityId remoteId = EntityId(UnsignedByteField<uint16_t>(5));
  FaultHandlerMock fhMock;
  LocalEntityCfg localEntityCfg(localId, IndicationCfg(), fhMock);
  FilesystemMock fsMock;
  UserMock userMock(fsMock);
  SeqCountProviderU16 seqCountProvider;
  SourceHandlerParams dp(localEntityCfg, userMock, seqCountProvider);

  EventReportingProxyMock eventReporterMock;
  LocalPool::LocalPoolConfig storeCfg = {{10, 32}, {10, 64}, {10, 128}, {10, 1024}};
  StorageManagerMock tcStore(2, storeCfg);
  StorageManagerMock tmStore(3, storeCfg);
  FsfwParams fp(tmReceiver, &mqMock, &eventReporterMock);
  fp.tcStore = &tcStore;
  fp.tmStore = &tmStore;
  auto sourceHandler = SourceHandler(dp, fp);

  RemoteEntityCfg cfg;
  cfg.maxFileSegmentLen = MAX_FILE_SEGMENT_SIZE;
  cfg.remoteId = remoteId;
  std::string srcFileName = "/tmp/cfdp-test.txt";
  std::string destFileName = "/tmp/cfdp-test2.txt";
  FilesystemParams srcFileNameFs(srcFileName.c_str());
  fsMock.createFile(srcFileNameFs);
  cfdp::StringLv srcNameLv(srcFileNameFs.path, std::strlen(srcFileNameFs.path));
  FilesystemParams destFileNameFs(destFileName.c_str());
  cfdp::StringLv destNameLv(destFileNameFs.path, std::strlen(destFileNameFs.path));
  PutRequest putRequest(remoteId, srcNameLv, destNameLv);
  CHECK(sourceHandler.initialize() == OK);

  auto onePduSentCheck = [&](const SourceHandler::FsmResult& fsmResult, TmTcMessage& tmtcMessage,
                             const uint8_t** pduPtr) {
    CHECK(fsmResult.errors == 0);
    CHECK(fsmResult.packetsSent == 1);
    CHECK(mqMock.numberOfSentMessages() == 1);
    REQUIRE(mqMock.getNextSentMessage(destQueueId, tmtcMessage) == OK);
    auto accessor = tmStore.getData(tmtcMessage.getStorageId());
    REQUIRE(accessor.first == OK);
    *pduPtr = accessor.second.data();
    return std::move(accessor);
  };
  auto genericMetadataCheck = [&](const SourceHandler::FsmResult& fsmResult,
                                  size_t expectedFileSize, uint16_t expectedSeqNum) {
    CHECK(fsmResult.errors == 0);
    CHECK(fsmResult.callStatus == CallStatus::CALL_AGAIN);
    TmTcMessage tmtcMessage;
    const uint8_t* pduPtr;
    auto accessor = onePduSentCheck(fsmResult, tmtcMessage, &pduPtr);
    CHECK(accessor.second.size() == 55);
    MetadataGenericInfo metadataInfo;
    MetadataPduReader metadataReader(pduPtr, accessor.second.size(), metadataInfo, nullptr, 0);
    REQUIRE(metadataReader.parseData() == OK);
    std::string srcNameRead = metadataReader.getSourceFileName().getString();
    CHECK(srcNameRead == srcFileName);
    TransactionSeqNum seqNum;
    metadataReader.getTransactionSeqNum(seqNum);
    CHECK(seqNum.getValue() == expectedSeqNum);
    CHECK(userMock.transactionIndicRecvd.size() == 1);
    CHECK(userMock.transactionIndicRecvd.back() == TransactionId(localId, seqNum));
    EntityId srcId;
    metadataReader.getSourceId(srcId);
    EntityId destId;
    metadataReader.getDestId(destId);
    CHECK(srcId.getValue() == localId.getValue());
    CHECK(destId.getValue() == remoteId.getValue());
    std::string destNameRead = metadataReader.getDestFileName().getString();
    CHECK(destNameRead == destFileName);
    if (expectedFileSize == 0) {
      CHECK(metadataInfo.getChecksumType() == ChecksumType::NULL_CHECKSUM);
    } else {
      CHECK(metadataInfo.getChecksumType() == ChecksumType::CRC_32);
    }
    CHECK(metadataInfo.getFileSize().value() == expectedFileSize);
    CHECK(!metadataInfo.isClosureRequested());
    mqMock.clearMessages();
  };
  auto genericEofCheck = [&](const SourceHandler::FsmResult& fsmResult, size_t expectedFileSize,
                             uint32_t expectedChecksum, uint16_t expectedSeqNum) {
    CHECK(fsmResult.errors == 0);
    CHECK(fsmResult.callStatus == CallStatus::CALL_AGAIN);
    TmTcMessage tmtcMessage;
    const uint8_t* pduPtr;
    auto accessor = onePduSentCheck(fsmResult, tmtcMessage, &pduPtr);
    // 10 byte PDU header, 1 byte directive field, 1 byte condition code, 4 byte checksum,
    // 4 byte FSS
    CHECK(accessor.second.size() == 20);
    EofInfo eofInfo;
    EofPduReader eofReader(pduPtr, accessor.second.size(), eofInfo);
    REQUIRE(eofReader.parseData() == OK);
    TransactionSeqNum seqNum;
    eofReader.getTransactionSeqNum(seqNum);
    CHECK(seqNum.getValue() == expectedSeqNum);
    auto transactionId = TransactionId(localId, seqNum);
    CHECK(userMock.eofSentRecvd.size() == 1);
    CHECK(userMock.eofSentRecvd.back() == transactionId);
    CHECK(eofInfo.getChecksum() == expectedChecksum);
    CHECK(eofInfo.getConditionCode() == ConditionCode::NO_ERROR);
    CHECK(eofInfo.getFileSize().value() == expectedFileSize);
  };
  auto genericNoticeOfCompletionCheck = [&](const SourceHandler::FsmResult& fsmResult,
                                            uint16_t expectedSeqNum) {
    CHECK(fsmResult.callStatus == CallStatus::DONE);
    CHECK(userMock.finishedRecvd.size() == 1);
    CHECK(userMock.finishedRecvd.back().first ==
          TransactionId(localId, TransactionSeqNum(cfdp::WidthInBytes::TWO_BYTES, expectedSeqNum)));
    CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::IDLE);
    CHECK(sourceHandler.getState() == CfdpState::IDLE);
  };

  SECTION("Test Basic") {
    CHECK(sourceHandler.getState() == CfdpState::IDLE);
    CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::IDLE);
  }

  SECTION("Transfer empty file") {
    CHECK(sourceHandler.transactionStart(putRequest, cfg) == OK);

    size_t expectedFileSize = 0;
    const SourceHandler::FsmResult& fsmResult = sourceHandler.stateMachine();
    // Verify metadata PDU was sent.
    genericMetadataCheck(fsmResult, expectedFileSize, 0);

    sourceHandler.stateMachine();
    // Verify EOF PDU was sent. No file data PDU is sent for an empty file and the checksum is 0.
    genericEofCheck(fsmResult, expectedFileSize, 0, 0);

    // Verify notice of completion.
    sourceHandler.stateMachine();
    genericNoticeOfCompletionCheck(fsmResult, 0);
  }

  SECTION("Transfer small file") {
    uint16_t expectedSeqNum = 0;
    fsMock.createFile(srcFileNameFs);
    std::string fileContent = "hello world\n";
    size_t expectedFileSize = fileContent.size();
    FileOpParams params(srcFileName.c_str(), expectedFileSize);
    fsMock.writeToFile(params, reinterpret_cast<const uint8_t*>(fileContent.data()));
    CHECK(sourceHandler.transactionStart(putRequest, cfg) == OK);
    const SourceHandler::FsmResult& fsmResult = sourceHandler.stateMachine();

    // Verify metadata PDU was sent.
    genericMetadataCheck(fsmResult, expectedFileSize, expectedSeqNum);

    // Verify that a small file data PDU was sent.
    sourceHandler.stateMachine();
    TmTcMessage tmtcMessage;
    const uint8_t* pduPtr;
    auto accessor = onePduSentCheck(fsmResult, tmtcMessage, &pduPtr);
    FileDataInfo fdInfo;
    FileDataReader fdReader(pduPtr, accessor.second.size(), fdInfo);
    // 10 byte PDU header, 4 byte offset, 12 bytes file data.
    CHECK(accessor.second.size() == 26);
    CHECK(fdReader.parseData() == OK);
    CHECK(fdInfo.getOffset().value() == 0);
    size_t fileSize = 0;
    const uint8_t* fileData = fdInfo.getFileData(&fileSize);
    REQUIRE(fileSize == fileContent.size());
    CHECK(fileData != nullptr);
    std::string dataReadBack(reinterpret_cast<const char*>(fileData), fileSize);
    CHECK(dataReadBack == fileContent);
    mqMock.clearMessages();

    sourceHandler.stateMachine();

    etl::crc32 crcCalc;
    crcCalc.add(fileContent.data(), fileContent.data() + fileContent.size());
    // Verify EOF PDU was sent.
    genericEofCheck(fsmResult, expectedFileSize, crcCalc.value(), expectedSeqNum);

    // Verify notice of completion.
    sourceHandler.stateMachine();
    genericNoticeOfCompletionCheck(fsmResult, expectedSeqNum);
  }

  SECTION("Transfer two segment file") {
    uint16_t expectedSeqNum = 0;
    // Create 400 bytes of random data. This should result in two file segments, with one
    // having the maximum size.
    std::random_device dev;
    std::mt19937 rng(dev());
    std::uniform_int_distribution<std::mt19937::result_type> distU8(0, 255);
    std::array<uint8_t, 400> largerFileData{};
    for (auto& val : largerFileData) {
      val = distU8(rng);
    }
    size_t expectedFileSize = largerFileData.size();
    fsMock.createFile(srcFileNameFs);
    FileOpParams params(srcFileName.c_str(), expectedFileSize);
    fsMock.writeToFile(params, reinterpret_cast<const uint8_t*>(largerFileData.data()));
    CHECK(sourceHandler.transactionStart(putRequest, cfg) == OK);
    const SourceHandler::FsmResult& fsmResult = sourceHandler.stateMachine();
    // Verify metadata PDU was sent.
    genericMetadataCheck(fsmResult, expectedFileSize, expectedSeqNum);

    // Check first file data PDU. It should have the maximum file segment size.
    sourceHandler.stateMachine();
    TmTcMessage tmtcMessage;
    const uint8_t* pduPtr;
    FileDataInfo fdInfo;
    {
      CHECK(fsmResult.callStatus == CallStatus::CALL_AGAIN);
      auto accessor = onePduSentCheck(fsmResult, tmtcMessage, &pduPtr);
      FileDataReader fdReader(pduPtr, accessor.second.size(), fdInfo);
      // 10 byte PDU header, 4 byte offset, 255 byte file data
      CHECK(accessor.second.size() == 269);
      CHECK(fdReader.parseData() == OK);
      CHECK(fdInfo.getOffset().value() == 0);
      size_t fileSize = 0;
      const uint8_t* fileData = fdInfo.getFileData(&fileSize);
      // Maximum file segment size.
      REQUIRE(fileSize == MAX_FILE_SEGMENT_SIZE);
      for (unsigned i = 0; i < fileSize; i++) {
        CHECK(fileData[i] == largerFileData[i]);
      }
    }
    mqMock.clearMessages();

    // Check second file data PDU.
    sourceHandler.stateMachine();
    {
      CHECK(fsmResult.callStatus == CallStatus::CALL_AGAIN);
      auto accessor = onePduSentCheck(fsmResult, tmtcMessage, &pduPtr);
      FileDataReader fdReader(pduPtr, accessor.second.size(), fdInfo);
      // 10 byte PDU header, 4 byte offset, remaining file data (400 - 255 == 145).
      CHECK(accessor.second.size() == 10 + 4 + largerFileData.size() - MAX_FILE_SEGMENT_SIZE);
      CHECK(fdReader.parseData() == OK);
      CHECK(fdInfo.getOffset().value() == MAX_FILE_SEGMENT_SIZE);
      size_t fileDataSize = 0;
      const uint8_t* fileData = fdInfo.getFileData(&fileDataSize);
      // Maximum file segment size.
      REQUIRE(fileDataSize == largerFileData.size() - MAX_FILE_SEGMENT_SIZE);
      for (unsigned i = 0; i < fileDataSize; i++) {
        CHECK(fileData[i] == largerFileData[MAX_FILE_SEGMENT_SIZE + i]);
      }
    }
    mqMock.clearMessages();

    // Check EOF and verify checksum.
    sourceHandler.stateMachine();

    etl::crc32 crcCalc;
    crcCalc.add(largerFileData.data(), largerFileData.data() + largerFileData.size());
    // Verify EOF PDU was sent.
    genericEofCheck(fsmResult, expectedFileSize, crcCalc.value(), expectedSeqNum);

    // Verify notice of completion.
    sourceHandler.stateMachine();
    genericNoticeOfCompletionCheck(fsmResult, expectedSeqNum);
  }
}