#include <cerrno>
#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <gmock/gmock.h>
#include <gpioplus/chip.hpp>
#include <gpioplus/test/sys.hpp>
#include <gtest/gtest.h>
#include <linux/gpio.h>
#include <memory>
#include <system_error>

namespace gpioplus
{
namespace
{

using testing::DoAll;
using testing::Return;
using testing::SaveArgPointee;
using testing::SetArgPointee;
using testing::StrEq;

TEST(LineFlagsTest, LineFlagsFromFlags)
{
    LineFlags line_flags(GPIOLINE_FLAG_KERNEL | GPIOLINE_FLAG_OPEN_DRAIN |
                         GPIOLINE_FLAG_OPEN_SOURCE);
    EXPECT_TRUE(line_flags.kernel);
    EXPECT_FALSE(line_flags.output);
    EXPECT_FALSE(line_flags.active_low);
    EXPECT_TRUE(line_flags.open_drain);
    EXPECT_TRUE(line_flags.open_source);

    line_flags = GPIOLINE_FLAG_IS_OUT | GPIOLINE_FLAG_ACTIVE_LOW;
    EXPECT_FALSE(line_flags.kernel);
    EXPECT_TRUE(line_flags.output);
    EXPECT_TRUE(line_flags.active_low);
    EXPECT_FALSE(line_flags.open_drain);
    EXPECT_FALSE(line_flags.open_source);
}

class ChipMethodTest : public testing::Test
{
  protected:
    const int expected_fd = 1234;
    testing::StrictMock<test::SysMock> mock;
    std::unique_ptr<Chip> chip;

    void SetUp()
    {
        const int chip_id = 1;
        const char* path = "/dev/gpiochip1";

        EXPECT_CALL(mock, open(StrEq(path), O_RDONLY | O_CLOEXEC))
            .WillOnce(Return(expected_fd));
        chip = std::make_unique<Chip>(chip_id, &mock);
    }

    void TearDown()
    {
        EXPECT_CALL(mock, close(expected_fd)).WillOnce(Return(0));
        chip.reset();
    }
};

TEST_F(ChipMethodTest, Basic)
{
    EXPECT_EQ(expected_fd, *chip->getFd());
    EXPECT_EQ(&mock, chip->getFd().getSys());
}

TEST_F(ChipMethodTest, GetChipInfoSuccess)
{
    const ChipInfo expected_info{"name", "label", 31};
    struct gpiochip_info info;
    ASSERT_LE(expected_info.name.size(), sizeof(info.name));
    strcpy(info.name, expected_info.name.c_str());
    ASSERT_LE(expected_info.label.size(), sizeof(info.label));
    strcpy(info.label, expected_info.label.c_str());
    info.lines = expected_info.lines;

    EXPECT_CALL(mock, gpio_get_chipinfo(expected_fd, testing::_))
        .WillOnce(DoAll(SetArgPointee<1>(info), Return(0)));
    ChipInfo retrieved_info = chip->getChipInfo();
    EXPECT_EQ(expected_info.name, retrieved_info.name);
    EXPECT_EQ(expected_info.label, retrieved_info.label);
    EXPECT_EQ(expected_info.lines, retrieved_info.lines);
}

TEST_F(ChipMethodTest, GetChipInfoFail)
{
    EXPECT_CALL(mock, gpio_get_chipinfo(expected_fd, testing::_))
        .WillOnce(Return(-EINVAL));
    EXPECT_THROW(chip->getChipInfo(), std::system_error);
}

TEST_F(ChipMethodTest, GetLineInfoSuccess)
{
    const uint32_t line = 176;
    const LineInfo expected_info{GPIOLINE_FLAG_ACTIVE_LOW, "name", "consumer"};
    struct gpioline_info info, req;
    info.flags = GPIOLINE_FLAG_ACTIVE_LOW;
    ASSERT_LE(expected_info.name.size(), sizeof(info.name));
    strcpy(info.name, expected_info.name.c_str());
    ASSERT_LE(expected_info.consumer.size(), sizeof(info.consumer));
    strcpy(info.consumer, expected_info.consumer.c_str());

    EXPECT_CALL(mock, gpio_get_lineinfo(expected_fd, testing::_))
        .WillOnce(
            DoAll(SaveArgPointee<1>(&req), SetArgPointee<1>(info), Return(0)));
    LineInfo retrieved_info = chip->getLineInfo(line);
    EXPECT_EQ(line, req.line_offset);
    EXPECT_FALSE(retrieved_info.flags.kernel);
    EXPECT_FALSE(retrieved_info.flags.output);
    EXPECT_TRUE(retrieved_info.flags.active_low);
    EXPECT_FALSE(retrieved_info.flags.open_drain);
    EXPECT_FALSE(retrieved_info.flags.open_source);
    EXPECT_EQ(expected_info.name, retrieved_info.name);
    EXPECT_EQ(expected_info.consumer, retrieved_info.consumer);
}

TEST_F(ChipMethodTest, GetLineInfoFail)
{
    const uint32_t line = 143;

    struct gpioline_info info;
    EXPECT_CALL(mock, gpio_get_lineinfo(expected_fd, testing::_))
        .WillOnce(DoAll(SaveArgPointee<1>(&info), Return(-EINVAL)));
    EXPECT_THROW(chip->getLineInfo(line), std::system_error);
    EXPECT_EQ(line, info.line_offset);
}

} // namespace
} // namespace gpioplus