/*!
 * \file test_ostream_dev.cpp
 *
 * Copyright (C) 2018 Christos Choutouridis
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <gtest/gtest.h>
#include <utl/dev/ostream_dev.h>
#include <array>

namespace test_ostream_dev {
   using namespace utl;

   // Device base data types
   // MUST be DefaultConstructible, Copyable
   using test_data_t = uint8_t;

   // Test data
   static const size_t SIZE = 10;

   test_data_t     Idata = 42;
   test_data_t&   IdataR = Idata;
   test_data_t&& IdataRR = 0xAA;
   test_data_t Ibuffer[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

   // ostream device implementer (Mocks)
   class Ostream_dev_impl : public ostream_dev<Ostream_dev_impl, test_data_t> {
      friend ostream_dev<Ostream_dev_impl, test_data_t>;
   public:
      // virtual device
      static constexpr size_t N =SIZE;
      std::array<test_data_t, N> v {};  // more than one so we can remember N-1 previous values
      size_t c {0};

   protected:
      // ostream_dev requirements
      size_t put_ (const test_data_t& data) {
         v[c++] = data;
         if (c >= N)
            c = 0;
         return 1;
      }
      size_t put_ (const test_data_t* data, size_t n) {
         for (size_t i=0 ; i<n && i<N; ++i) {
            v[i] = data[i];
         }
         return n;
      }
   public:
      test_data_t& getLastV () { return (c) ? v[c-1] : v[N-1]; }
   };
   // virtual ostream device
   class Ostream_vdev_impl : public ostream_dev<virtual_tag, test_data_t> {
   public:
      // virtual device
      static constexpr size_t N =SIZE;
      std::array<test_data_t, N> v {};
      size_t c {0};

   protected:
      // ostream_dev requirements
      size_t put_ (const test_data_t& data) {
         v[c++] = data;
         if (c >= N)
            c = 0;
         return 1;
      }
      size_t put_ (const test_data_t* data, size_t n) {
         for (size_t i=0 ; i<n && i<N; ++i) {
            v[i] = data[i];
         }
         return n;
      }
   public:
      test_data_t& getLastV () { return (c) ? v[c-1] : v[N-1]; }
   };

   // fixtures
   class Tostream_Idev : public ::testing::Test {
   protected:
      // zero initialized device
      Ostream_dev_impl osIdev {};
   };
   class Tostream_Vdev : public ::testing::Test {
   protected:
      // zero initialized devices
      std::array<Ostream_vdev_impl, 5> osVdev {};
      ostream_dev<virtual_tag, test_data_t> *basePointer = nullptr;
   };

   //ToDo: Must add Concept test for ostream_dev
//   TEST_F(Tostream_Idev, TestConcept) {
//      EXPECT_EQ(true, Ostream_dev<osIdev>);
//   }

   TEST_F(Tostream_Idev, Construction) {
      EXPECT_LT(0UL, sizeof(osIdev));
   }

   TEST_F (Tostream_Idev, Api) {
      EXPECT_EQ(1UL, osIdev.put(Idata)); // single write from var
      EXPECT_EQ(Idata, osIdev.getLastV ());

      EXPECT_EQ(1UL, osIdev.put(IdataR));// single write from lvalue ref
      EXPECT_EQ(IdataR, osIdev.getLastV ());

      EXPECT_EQ(1UL, osIdev.put(IdataRR));// single write from rvalue ref
      EXPECT_EQ(IdataRR, osIdev.getLastV ());

      EXPECT_EQ(1UL, osIdev.put(42));// single write from rvalue
      EXPECT_EQ(42U, osIdev.getLastV ());

      EXPECT_EQ(7U, osIdev.put(Ibuffer, 7)); // batch write some data
      for (size_t i =0 ; i<7 ; ++i) {
         EXPECT_EQ(Ibuffer[i], osIdev.v[i]);
      }
      EXPECT_EQ(0, osIdev.v[7]);

      EXPECT_EQ(SIZE, osIdev.put(Ibuffer, SIZE));   // batch write all data
      for (size_t i =0 ; i<SIZE ; ++i) {
         EXPECT_EQ(Ibuffer[i], osIdev.v[i]);
      }
   }

   TEST_F (Tostream_Idev, streamOperator) {
      struct Blop {
         test_data_t x, y, z;
      };

      osIdev << Idata; // single write from var
      EXPECT_EQ(Idata, osIdev.getLastV ());

      osIdev << IdataR;// single write from lvalue ref
      EXPECT_EQ(IdataR, osIdev.getLastV ());

      osIdev << IdataRR;// single write from rvalue ref
      EXPECT_EQ(IdataRR, osIdev.getLastV ());

      osIdev << (test_data_t)42;  // single write from rvalue
      EXPECT_EQ(42U, osIdev.getLastV ());

      // stream a blop of data (should use put(T*, N) version)
      Blop blop {1, 1, 42};
      osIdev << blop;
      EXPECT_EQ(1U, osIdev.v[0]);
      EXPECT_EQ(1U, osIdev.v[1]);
      EXPECT_EQ(42U, osIdev.v[2]);

      const Blop cblop {2, 2, 42};
      osIdev << cblop;
      EXPECT_EQ(2U, osIdev.v[0]);
      EXPECT_EQ(2U, osIdev.v[1]);
      EXPECT_EQ(42U, osIdev.v[2]);
   }

   TEST_F (Tostream_Idev, Iterator1) {
      // default constructible
      Ostream_dev_impl::iterator def_it;
      EXPECT_EQ (0, *(int32_t*)&def_it);  // not dereferencable

      // output iterator requirements
      // https://en.cppreference.com/w/cpp/named_req/OutputIterator
      auto it = osIdev.begin();
      auto it2(it);
      EXPECT_EQ (*(int32_t*)&it, *(int32_t*)&it2);

      it2 = it;
      EXPECT_EQ (*(int32_t*)&it, *(int32_t*)&it2);
      def_it = it;
      EXPECT_EQ (*(int32_t*)&it, *(int32_t*)&def_it);
      ++it, it++;

      *it = Idata;
      EXPECT_EQ (Idata, osIdev.getLastV ());
      *it = IdataR;
      EXPECT_EQ (IdataR, osIdev.getLastV ());
      *it = IdataRR;
      EXPECT_EQ (IdataRR, osIdev.getLastV ());

      *it++ = Idata;
      EXPECT_EQ (Idata, osIdev.getLastV ());

   }

   TEST_F (Tostream_Idev, Iterator2) {
         auto it = osIdev.begin();

         std::fill_n(it, SIZE, Idata);
         for (size_t i=0 ;i<SIZE ; ++i) {
            EXPECT_EQ (Idata, osIdev.v[i]);
         }
      }

   TEST_F (Tostream_Vdev, virtualApi) {
      // loop to virtual devices and use them via base pointer
      for (auto& dev : osVdev) {
         basePointer = &dev;
         basePointer->put(Idata);
         EXPECT_EQ(Idata, dev.v[0]);

         EXPECT_EQ(SIZE, basePointer->put(Ibuffer, SIZE));   // batch write all data
         for (size_t i =0 ; i<SIZE ; ++i) {
            EXPECT_EQ(Ibuffer[i], dev.v[i]);
         }
      }
   }

   TEST_F (Tostream_Vdev, virtualStream) {
         struct Blop {
            test_data_t x, y, z;
         };
         Blop blop {1, 1, 42};
         const Blop cblop {2, 2, 42};

         // loop to virtual devices and use them via base pointer
         for (auto& dev : osVdev) {
            basePointer = &dev;
            *basePointer << IdataR;
            EXPECT_EQ(IdataR, dev.v[0]);

            *basePointer << blop;
            EXPECT_EQ(1U, dev.v[0]);
            EXPECT_EQ(1U, dev.v[1]);
            EXPECT_EQ(42U, dev.v[2]);

            *basePointer << cblop;
            EXPECT_EQ(2U, dev.v[0]);
            EXPECT_EQ(2U, dev.v[1]);
            EXPECT_EQ(42U, dev.v[2]);
         }
      }

   TEST_F (Tostream_Vdev, virtualIterator) {
         // loop to virtual devices and use them via base pointer
         for (auto& dev : osVdev) {
            basePointer = &dev;
            auto it = *basePointer->begin();
            std::fill_n(it, SIZE, Idata);
            for (size_t i=0 ;i<SIZE ; ++i) {
               EXPECT_EQ (Idata, dev.v[i]);
            }
         }
      }
}
