#include "common/common_pch.h"

#include <boost/range/adaptors.hpp>

#include "common/construct.h"
#include "common/strings/utf8.h"
#include "common/unique_numbers.h"
#include "propedit/attachment_target.h"

#include "tests/unit/init.h"
#include "tests/unit/util.h"

namespace {

using namespace libmatroska;
using namespace mtx::construct;
using namespace mtxut;

TEST(AttachmentTarget, Basics) {
  attachment_target_c at;

  EXPECT_TRUE(at.has_changes());
}

TEST(AttachmentTarget, ParsingCorrectArgumentsForAdd) {
  attachment_target_c at;
  attachment_target_c::options_t no_opt;

  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_add, "bla.txt", no_opt));
  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_add, "bla:blubb.txt", no_opt));
}

TEST(AttachmentTarget, ParsingCorrectArgumentsForDelete) {
  attachment_target_c at;
  attachment_target_c::options_t no_opt;

  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_delete, "123123123",                    no_opt));
  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_delete, "=123123123",                   no_opt));

  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_delete, "name:blubb",                   no_opt));
  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_delete, "mime-type:doesnotexist/blubb", no_opt));

  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_delete, "NAME:blubb",                   no_opt));
  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_delete, "MImE-tYpe:doesnotexist/blubb", no_opt));
}

TEST(AttachmentTarget, ParsingCorrectArgumentsForReplace) {
  attachment_target_c at;
  attachment_target_c::options_t no_opt;

  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, "123123123:bla.txt",                    no_opt));
  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, "=123123123:bla.txt",                   no_opt));

  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, "name:blubb:bla.txt",                   no_opt));
  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, "mime-type:doesnotexist/blubb:bla.txt", no_opt));

  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, "NAME:blubb:bla.txt",                   no_opt));
  EXPECT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, "MImE-tYpe:doesnotexist/blubb:bla.txt", no_opt));
}

TEST(AttachmentTarget, ParsingInvalidArgumentsForAdd) {
  attachment_target_c at;
  attachment_target_c::options_t no_opt;

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_add, "", no_opt), std::invalid_argument);
}

TEST(AttachmentTarget, ParsingInvalidArgumentsForDelete) {
  attachment_target_c at;
  attachment_target_c::options_t no_opt;

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "",            no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "=",           no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "-",           no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "=-",          no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "-123",        no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "=-123",       no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "123:",        no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "=123:",       no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "123:abc",     no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "=123:abc",    no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "name:",       no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "mime-type:",  no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_delete, "gonzo:peter", no_opt), std::invalid_argument);
}

TEST(AttachmentTarget, ParsingInvalidArgumentsForReplace) {
  attachment_target_c at;
  attachment_target_c::options_t no_opt;

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "",                        no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "=",                       no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "-",                       no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "=-",                      no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "-123",                    no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "=-123",                   no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "123:",                    no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "=123:",                   no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "name:bla",                no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "mime-type:chunky/bacon",  no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "name:bla:",               no_opt), std::invalid_argument);
  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "mime-type:chunky/bacon:", no_opt), std::invalid_argument);

  EXPECT_THROW(at.parse_spec(attachment_target_c::ac_replace, "gonzo:peter:wuff",        no_opt), std::invalid_argument);
}

TEST(AttachmentTarget, ValidateOk) {
  attachment_target_c at;
  attachment_target_c::options_t no_opt;

  ASSERT_NO_THROW(at.parse_spec(attachment_target_c::ac_delete, "1", no_opt));
  ASSERT_NO_THROW(at.validate());

  at = attachment_target_c{};

  ASSERT_NO_THROW(at.parse_spec(attachment_target_c::ac_add, "tests/unit/data/text/chunky_bacon.txt", no_opt));
  ASSERT_NO_THROW(at.validate());

  at = attachment_target_c{};

  ASSERT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, "1:tests/unit/data/text/chunky_bacon.txt", no_opt));
  ASSERT_NO_THROW(at.validate());
}

TEST(AttachmentTarget, ValidateFailure) {
  attachment_target_c at;
  attachment_target_c::options_t no_opt;

  ASSERT_NO_THROW(at.parse_spec(attachment_target_c::ac_add, "doesnotexist", no_opt));
  ASSERT_THROW(at.validate(), mtxut::mxerror_x);

  at = attachment_target_c{};

  ASSERT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, "1:doesnotexist", no_opt));
  ASSERT_THROW(at.validate(), mtxut::mxerror_x);
}

EbmlMaster *
cons_att(std::string const &content,
         std::string const &name,
         std::string const &mime_type,
         uint64_t uid,
         std::string const &description = "") {
  return cons<KaxAttached>(new KaxFileName,                                         to_wide(name),
                           !description.empty() ? new KaxFileDescription : nullptr, to_wide(description),
                           new KaxMimeType,                                         mime_type,
                           new KaxFileUID,                                          uid,
                           new KaxFileData,                                         memory_c::clone(content.c_str(), content.length()));
}

EbmlMaster *
cons_default_atts() {
  return cons<KaxAttachments>(cons_att("Hello World\nThis is fun.\nChunky Bacon!!",        "Dummy File.txt",       "text/plain",                47110815, "Some funky description"),
                              cons_att("<html><body><h1>Chunky Bacon!</h2></body></html>", "chunky bacon.html",    "text/html",                 123454321),
                              cons_att("198273498725987195610824371289567129357",          "filename:with:colons", "application/otctet-stream", 99887766),
                              cons_att("<html><body><h1>Chunky Bacon!</h2></body></html>", "chunky bacon.html",    "text/html",                 918273645));
}

void
test_add(std::function<void(ebml_master_cptr &l1_b, attachment_target_c::options_t &opt)> local_setup) {
  g_warning_issued = false;

  clear_list_of_unique_numbers(UNIQUE_ALL_IDS);

  auto l1_a = ebml_element_cptr{cons_default_atts()};
  auto l1_b = ebml_master_cptr{cons_default_atts()};

  attachment_target_c at;
  attachment_target_c::options_t opt;

  local_setup(l1_b, opt);

  at.set_level1_element(l1_a);

  ASSERT_NO_THROW(at.parse_spec(attachment_target_c::ac_add, "tests/unit/data/text/chunky_bacon.txt", opt));
  ASSERT_NO_THROW(at.validate());
  ASSERT_NO_THROW(at.execute());
  ASSERT_FALSE(g_warning_issued);
  EXPECT_EBML_EQ(*l1_a, *l1_b);
}

TEST(AttachmentTarget, AddDetectAll) {
  test_add([](ebml_master_cptr &l1_b, attachment_target_c::options_t &) {
      l1_b->PushElement(*cons_att("Chunky Bacon\n", "chunky_bacon.txt", "text/plain", 1));
    });
}

TEST(AttachmentTarget, AddProvideName) {
  test_add([](ebml_master_cptr &l1_b, attachment_target_c::options_t &opt) {
      opt.name("Charlie!");
      l1_b->PushElement(*cons_att("Chunky Bacon\n", "Charlie!", "text/plain", 1));
    });
}

TEST(AttachmentTarget, AddProvideMIMEType) {
  test_add([](ebml_master_cptr &l1_b, attachment_target_c::options_t &opt) {
      opt.mime_type("inva/lid");
      l1_b->PushElement(*cons_att("Chunky Bacon\n", "chunky_bacon.txt", "inva/lid", 1));
    });
}

TEST(AttachmentTarget, AddProvideDescription) {
  test_add([](ebml_master_cptr &l1_b, attachment_target_c::options_t &opt) {
      opt.description("somewhere...");
      l1_b->PushElement(*cons_att("Chunky Bacon\n", "chunky_bacon.txt", "text/plain", 1, "somewhere..."));
    });
}

TEST(AttachmentTarget, AddProvideAll) {
  test_add([](ebml_master_cptr &l1_b, attachment_target_c::options_t &opt) {
      opt.mime_type("empty/rooms").description("where we learn to live").name("stuffy.h");
      l1_b->PushElement(*cons_att("Chunky Bacon\n", "stuffy.h", "empty/rooms", 1, "where we learn to live"));
    });
}

void
test_delete(std::string const &spec,
            std::vector<int> expected_deletions,
            bool expect_warning) {
  g_warning_issued = false;

  auto l1_a = ebml_element_cptr{cons_default_atts()};
  auto l1_b = ebml_master_cptr{cons_default_atts()};

  attachment_target_c at;
  at.set_id_manager(std::make_shared<attachment_id_manager_c>(static_cast<EbmlMaster *>(l1_a.get()), 1));
  at.set_level1_element(l1_a);

  attachment_target_c::options_t opt;

  std::sort(expected_deletions.begin(), expected_deletions.end());
  std::reverse(expected_deletions.begin(), expected_deletions.end());
  for (auto idx : expected_deletions) {
    delete (*l1_b)[idx];
    l1_b->Remove(idx);
  }

  ASSERT_NO_THROW(at.parse_spec(attachment_target_c::ac_delete, spec, opt));
  ASSERT_NO_THROW(at.validate());
  ASSERT_NO_THROW(at.execute());
  ASSERT_EQ(g_warning_issued, expect_warning);
  EXPECT_EBML_EQ(*l1_a, *l1_b);
}

TEST(AttachmentTarget, DeleteById) {
  test_delete("1", { 0 }, false);
  test_delete("4", { 3 }, false);

  test_delete("0", { }, true);
  test_delete("5", { }, true);
}

TEST(AttachmentTarget, DeleteByUid) {
  test_delete("=47110815",  { 0 }, false);
  test_delete("=918273645", { 3 }, false);

  test_delete("=888888888", { }, true);
  test_delete("=0",         { }, true);
}

TEST(AttachmentTarget, DeleteByName) {
  test_delete("name:Dummy File.txt",       { 0    }, false);
  test_delete("NaMe:filename:with:colons", { 2    }, false);
  test_delete("NAME:chunky bacon.html",    { 1, 3 }, false);

  test_delete("name:doesnotexist", { }, true);
}

TEST(AttachmentTarget, DeleteByMimeType) {
  test_delete("mime-type:text/plain", { 0    }, false);
  test_delete("Mime-Type:text/html",  { 1, 3 }, false);

  test_delete("mime-type:doesnotexist", { }, true);
}

void
test_replace(std::string const &custom_message,
             std::string const &spec,
             std::vector<int> expected_replacements,
             bool expect_warning,
             std::function<void(EbmlMaster &b, attachment_target_c::options_t &opt)> local_setup = [](EbmlMaster &, attachment_target_c::options_t &) { }) {
  g_warning_issued = false;

  auto l1_a = ebml_element_cptr{cons_default_atts()};
  auto l1_b = ebml_master_cptr{cons_default_atts()};

  attachment_target_c at;
  at.set_id_manager(std::make_shared<attachment_id_manager_c>(static_cast<EbmlMaster *>(l1_a.get()), 1));
  at.set_level1_element(l1_a);

  static std::string const expected_content{"Chunky Bacon\n"};

  for (auto idx : expected_replacements) {
    auto child = (*l1_b)[idx];
    if (!dynamic_cast<EbmlMaster *>(child))
      continue;

    auto data = find_child<KaxFileData>(static_cast<EbmlMaster &>(*child));
    if (data)
      data->CopyBuffer(reinterpret_cast<binary const *>(expected_content.c_str()), expected_content.length());
  }

  attachment_target_c::options_t opt;
  local_setup(*l1_b, opt);

  auto message = "replace by "s + custom_message;

  ASSERT_NO_THROW(at.parse_spec(attachment_target_c::ac_replace, spec + ":tests/unit/data/text/chunky_bacon.txt", opt)) << message;
  ASSERT_NO_THROW(at.validate())                                                                                        << message;
  ASSERT_NO_THROW(at.execute())                                                                                         << message;
  ASSERT_EQ(g_warning_issued, expect_warning)                                                                           << message;
  EXPECT_EBML_EQ(*l1_a, *l1_b)                                                                                          << message;
}

  // return cons<KaxAttachments>(cons_att("Hello World\nThis is fun.\nChunky Bacon!!",        "Dummy File.txt",       "text/plain",                47110815, "Some funky description"),
  //                             cons_att("<html><body><h1>Chunky Bacon!</h2></body></html>", "chunky bacon.html",    "text/html",                 123454321),
  //                             cons_att("198273498725987195610824371289567129357",          "filename:with:colons", "application/otctet-stream", 99887766),
  //                             cons_att("<html><body><h1>Chunky Bacon!</h2></body></html>", "chunky bacon.html",    "text/html",                 918273645));

TEST(AttachmentTarget, ReplaceById) {
  test_replace("ID #1", "3", { 2 }, false);

  test_replace("ID #2", "3", { 2 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      auto att = static_cast<EbmlMaster *>(b[2]);

      opt.description("").name("").mime_type("");

      delete_children<KaxFileDescription>(att);
      get_child<KaxFileName>(att).SetValueUTF8("chunky_bacon.txt");
      get_child<KaxMimeType>(att).SetValue("text/plain");
    });

  test_replace("ID #3", "3", { 2 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      auto att = static_cast<EbmlMaster *>(b[2]);

      opt.description("Moocow").name("miau&wuff.html").mime_type("bacon/chunky");

      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");
    });

  test_replace("ID #4", "3", { 2 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      auto att = static_cast<EbmlMaster *>(b[2]);

      opt.description("Moocow");

      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
    });
}

TEST(AttachmentTarget, ReplaceByUid) {
  test_replace("UID #1", "=99887766", { 2 }, false);

  test_replace("UID #2", "=99887766", { 2 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      auto att = static_cast<EbmlMaster *>(b[2]);

      opt.description("").name("").mime_type("");

      delete_children<KaxFileDescription>(att);
      get_child<KaxFileName>(att).SetValueUTF8("chunky_bacon.txt");
      get_child<KaxMimeType>(att).SetValue("text/plain");
    });

  test_replace("UID #3", "=99887766", { 2 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      auto att = static_cast<EbmlMaster *>(b[2]);

      opt.description("Moocow").name("miau&wuff.html").mime_type("bacon/chunky");

      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");
    });
}

TEST(AttachmentTarget, ReplaceByName) {
  test_replace("name #1", "name:chunky bacon.html", { 1, 3 }, false);

  test_replace("name #2", "name:chunky bacon.html", { 1, 3 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      opt.description("").name("").mime_type("");

      auto att = static_cast<EbmlMaster *>(b[1]);
      delete_children<KaxFileDescription>(att);
      get_child<KaxFileName>(att).SetValueUTF8("chunky_bacon.txt");
      get_child<KaxMimeType>(att).SetValue("text/plain");

      att = static_cast<EbmlMaster *>(b[3]);
      delete_children<KaxFileDescription>(att);
      get_child<KaxFileName>(att).SetValueUTF8("chunky_bacon.txt");
      get_child<KaxMimeType>(att).SetValue("text/plain");
    });

  test_replace("name #3", "name:chunky bacon.html", { 1, 3 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      opt.description("Moocow").name("miau&wuff.html").mime_type("bacon/chunky");

      auto att = static_cast<EbmlMaster *>(b[1]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");

      att = static_cast<EbmlMaster *>(b[3]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");
    });

  test_replace("name #4", "naME:chunky bacon.html", { 1, 3 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      opt.description("Moocow").name("miau&wuff.html").mime_type("bacon/chunky");

      auto att = static_cast<EbmlMaster *>(b[1]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");

      att = static_cast<EbmlMaster *>(b[3]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");
    });

  test_replace("name #5", "naME:filename\\cwith\\ccolons", { 2 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      opt.description("Moocow").name("miau&wuff.html").mime_type("bacon/chunky");

      auto att = static_cast<EbmlMaster *>(b[2]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");
    });
}

TEST(AttachmentTarget, ReplaceByMimeType) {
  test_replace("MIME type #1", "mime-type:text/html", { 1, 3 }, false);

  test_replace("MIME type #2", "mime-type:text/html", { 1, 3 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      opt.description("").name("").mime_type("");

      auto att = static_cast<EbmlMaster *>(b[1]);
      delete_children<KaxFileDescription>(att);
      get_child<KaxFileName>(att).SetValueUTF8("chunky_bacon.txt");
      get_child<KaxMimeType>(att).SetValue("text/plain");

      att = static_cast<EbmlMaster *>(b[3]);
      delete_children<KaxFileDescription>(att);
      get_child<KaxFileName>(att).SetValueUTF8("chunky_bacon.txt");
      get_child<KaxMimeType>(att).SetValue("text/plain");
    });

  test_replace("MIME type #3", "mime-type:text/html", { 1, 3 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      opt.description("Moocow").name("miau&wuff.html").mime_type("bacon/chunky");

      auto att = static_cast<EbmlMaster *>(b[1]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");

      att = static_cast<EbmlMaster *>(b[3]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");
    });

  test_replace("MIME type #4", "miMe-TYPE:text/html", { 1, 3 }, false, [](EbmlMaster &b, attachment_target_c::options_t &opt) {
      opt.description("Moocow").name("miau&wuff.html").mime_type("bacon/chunky");

      auto att = static_cast<EbmlMaster *>(b[1]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");

      att = static_cast<EbmlMaster *>(b[3]);
      get_child<KaxFileDescription>(att).SetValueUTF8("Moocow");
      get_child<KaxFileName>(att).SetValueUTF8("miau&wuff.html");
      get_child<KaxMimeType>(att).SetValue("bacon/chunky");
    });
}

}
