/**
 * Copyright © 2020 IBM Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "extensions/openpower-pels/device_callouts.hpp"
#include "extensions/openpower-pels/paths.hpp"

#include <fstream>

#include <gtest/gtest.h>

using namespace openpower::pels;
using namespace openpower::pels::device_callouts;
namespace fs = std::filesystem;

// The callout JSON looks like:
// "I2C":
//   "<bus>":
//     "<address>":
//       "Callouts": ...
//
// "FSI":
//   "<fsi link>":
//     "Callouts": ...
//
// "FSI-I2C":
//    "<fsi link>":
//      "<bus>":
//        "<address>":
//          "Callouts": ...
//
// "FSI-SPI":
//    "<fsi link>":
//      "<bus>":
//        "Callouts": ...

const auto calloutJSON = R"(
{
    "I2C":
    {
        "0":
        {
            "32":
            {
                "Callouts":[
                    {
                       "Name": "/chassis/motherboard/cpu0",
                       "LocationCode": "P1-C19",
                       "Priority": "H"
                    }
                ],
                "Dest": "proc-0 target"
            },
            "81":
            {
                "Callouts":[
                    {
                       "Name": "/chassis/motherboard/cpu0",
                       "LocationCode": "P1-C19",
                       "Priority": "H"
                    }
                ],
                "Dest": "proc-0 target"
            },
            "90":
            {
                "Callouts":[
                    {
                       "Name": "This is missing the location code",
                       "Priority": "H"
                    }
                ],
                "Dest": "proc-0 target"
            }
        },
        "14":
        {
            "112":
            {
                "Callouts":[
                    {
                       "Name": "/chassis/motherboard/cpu0",
                       "LocationCode": "P1-C19",
                       "Priority": "H"
                    }
                ],
                "Dest": "proc-0 target"
            },
            "114":
            {
                "Callouts":[
                    {
                       "Name": "/chassis/motherboard/cpu0",
                       "LocationCode": "P1-C19",
                       "Priority": "H",
                       "MRU": "core0"
                    },
                    {
                       "Name": "/chassis/motherboard",
                       "LocationCode": "P1",
                       "Priority": "M"
                    }
                ],
                "Dest": "proc-0 target"
            }
        }
    },
    "FSI":
    {
        "0":
        {
           "Callouts":[
                {
                    "Name": "/chassis/motherboard/cpu0",
                    "LocationCode": "P1-C19",
                    "Priority": "H"
                }
           ],
           "Dest": "proc-0 target"
        },
        "0-1":
        {
           "Callouts":[
                {
                    "Name": "/chassis/motherboard/cpu0",
                    "LocationCode": "P1-C19",
                    "Priority": "H",
                    "MRU": "core"
                }
           ],
           "Dest": "proc-0 target"
        }
    },
    "FSI-I2C":
    {
        "0-3":
        {
           "7":
           {
              "24":
              {
                 "Callouts":[
                    {
                       "Name": "/chassis/motherboard/cpu0",
                       "LocationCode": "P1-C19",
                       "Priority": "H"
                    }
                 ],
                 "Dest": "proc-0 target"
              },
              "25":
              {
                 "Callouts":[
                    {
                       "Name": "/chassis/motherboard/cpu5",
                       "LocationCode": "P1-C25",
                       "Priority": "H"
                    },
                    {
                       "Name": "/chassis/motherboard",
                       "LocationCode": "P1",
                       "Priority": "M"
                    },
                    {
                        "Name": "/chassis/motherboard/bmc",
                        "LocationCode": "P2",
                        "Priority": "L"
                    }
                 ],
                 "Dest": "proc-5 target"
              }
           }
        }
    },
    "FSI-SPI":
    {
        "8":
        {
            "3":
            {
                "Callouts":[
                    {
                       "Name": "/chassis/motherboard/cpu0",
                       "LocationCode": "P1-C19",
                       "Priority": "H"
                    }
                ],
                "Dest": "proc-0 target"
            },
            "4":
            {
                "Callouts":[
                    {
                       "Name": "/chassis/motherboard/cpu2",
                       "LocationCode": "P1-C12",
                       "Priority": "M"
                    }
                ],
                "Dest": "proc-0 target"
            }
        }
    }
})"_json;

class DeviceCalloutsTest : public ::testing::Test
{
  public:
    static void SetUpTestCase()
    {
        dataPath = getPELReadOnlyDataPath();
        std::ofstream file{dataPath / filename};
        file << calloutJSON.dump();
    }

    static void TearDownTestCase()
    {
        fs::remove_all(dataPath);
    }

    static std::string filename;
    static fs::path dataPath;
};

std::string DeviceCalloutsTest::filename = "systemA_dev_callouts.json";
fs::path DeviceCalloutsTest::dataPath;

namespace openpower::pels::device_callouts
{

// Helpers to compair vectors of Callout objects
bool operator!=(const Callout& left, const Callout& right)
{
    return (left.priority != right.priority) ||
           (left.locationCode != right.locationCode) ||
           (left.name != right.name) || (left.mru != right.mru) ||
           (left.debug != right.debug);
}

bool operator==(const std::vector<Callout>& left,
                const std::vector<Callout>& right)
{
    if (left.size() != right.size())
    {
        return false;
    }

    for (size_t i = 0; i < left.size(); i++)
    {
        if (left[i] != right[i])
        {
            return false;
        }
    }

    return true;
}

} // namespace openpower::pels::device_callouts

// Test looking up the JSON file based on the system compatible names
TEST_F(DeviceCalloutsTest, getJSONFilenameTest)
{
    {
        std::vector<std::string> compatibles{"system1", "systemA", "system3"};
        EXPECT_EQ(util::getJSONFilename(compatibles),
                  fs::path{dataPath / filename});
    }

    // Actual filename not in compatibles
    {
        std::vector<std::string> compatibles{"system5", "system6"};
        EXPECT_THROW(util::getJSONFilename(compatibles), std::invalid_argument);
    }

    // Test using the fallback name
    {
        // If _dev_callouts.json is there, it will be used if no other
        // match is found.
        fs::path fallbackFile{dataPath / "_dev_callouts.json"};
        std::ofstream file{fallbackFile};
        file << calloutJSON.dump();
        file.close();

        // Fallback shouldn't be used because the actual systemA file is there
        {
            std::vector<std::string> compatibles{"system1", "systemA",
                                                 "system3"};
            EXPECT_EQ(util::getJSONFilename(compatibles),
                      fs::path{dataPath / filename});
        }

        // Fallback should be used because no other match
        {
            std::vector<std::string> compatibles{"systemX", "systemY"};
            EXPECT_EQ(util::getJSONFilename(compatibles), fallbackFile);
        }
    }
}

// Test determining the callout type from the device path
TEST_F(DeviceCalloutsTest, getCalloutTypeTest)
{

    // Invalid
    {
        EXPECT_EQ(util::getCalloutType("/some/bad/device/path"),
                  util::CalloutType::unknown);
    }

    // I2C
    {
        EXPECT_EQ(util::getCalloutType(
                      "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
                      "1e78a340.i2c-bus/i2c-14/14-0072"),
                  util::CalloutType::i2c);
    }

    // FSI
    {
        EXPECT_EQ(util::getCalloutType(
                      "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                      "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
                      "slave@01:00/01:01:00:06/sbefifo2-dev0/occ-hwmon.2"),
                  util::CalloutType::fsi);
    }

    // FSI-I2C
    {
        EXPECT_EQ(util::getCalloutType(
                      "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                      "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
                      "slave@01:00/01:01:00:03/i2c-211/211-0055"),
                  util::CalloutType::fsii2c);
    }

    // FSI-SPI
    {

        EXPECT_EQ(
            util::getCalloutType(
                "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/fsi-master/"
                "fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/slave@08:00/"
                "01:03:00:04/spi_master/spi9/spi9.0/spi9.00/nvmem"),
            util::CalloutType::fsispi);
    }
}

// Test getting I2C search keys
TEST_F(DeviceCalloutsTest, getI2CSearchKeysTest)
{

    {
        EXPECT_EQ(util::getI2CSearchKeys(
                      "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
                      "1e78a340.i2c-bus/i2c-10/10-0022"),
                  (std::tuple{10, 0x22}));

        EXPECT_EQ(util::getI2CSearchKeys(
                      "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                      "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
                      "slave@01:00/01:01:00:03/i2c-211/211-0055"),
                  (std::tuple{11, 0x55}));
    }

    {
        EXPECT_THROW(util::getI2CSearchKeys("/sys/some/bad/path"),
                     std::invalid_argument);
    }
}
//
// Test getting SPI search keys
TEST_F(DeviceCalloutsTest, getSPISearchKeysTest)
{
    {
        EXPECT_EQ(
            util::getSPISearchKeys(
                "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/fsi-master/"
                "fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/slave@08:00/"
                "01:03:00:04/spi_master/spi9/spi9.0/spi9.00/nvmem"),
            9);
    }

    {
        EXPECT_THROW(util::getSPISearchKeys("/sys/some/bad/path"),
                     std::invalid_argument);
    }
}

// Test getting FSI search keys
TEST_F(DeviceCalloutsTest, getFSISearchKeysTest)
{
    {
        EXPECT_EQ(util::getFSISearchKeys(
                      "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                      "fsi-master/fsi0/slave@00:00/00:00:00:04/spi_master/spi2/"
                      "spi2.0/spi2.00/nvmem"),
                  "0");
    }

    {
        EXPECT_EQ(util::getFSISearchKeys(
                      "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                      "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
                      "slave@01:00/01:01:00:06/sbefifo2-dev0/occ-hwmon.2"),
                  "0-1");
    }

    {
        EXPECT_EQ(
            util::getFSISearchKeys(
                "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
                "slave@01:00/01:01:00:0a:/fsi-master/slave@04:00/01:01:00:0a"),
            "0-1-4");
    }

    {
        EXPECT_THROW(util::getFSISearchKeys("/sys/some/bad/path"),
                     std::invalid_argument);
    }
}

// Test getting FSI-I2C search keys
TEST_F(DeviceCalloutsTest, getFSII2CSearchKeysTest)
{
    {
        // Link 0-1 bus 11 address 0x55
        EXPECT_EQ(util::getFSII2CSearchKeys(
                      "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                      "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
                      "slave@01:00/01:01:00:03/i2c-211/211-0055"),
                  (std::tuple{"0-1", std::tuple{11, 0x55}}));
    }

    {
        EXPECT_THROW(util::getFSII2CSearchKeys("/sys/some/bad/path"),
                     std::invalid_argument);
    }
}

// Test getting FSI-SPI search keys
TEST_F(DeviceCalloutsTest, getFSISPISearchKeysTest)
{
    {
        // Link 0-8 SPI 9
        EXPECT_EQ(
            util::getFSISPISearchKeys(
                "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/fsi-master/"
                "fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/slave@08:00/"
                "01:03:00:04/spi_master/spi9/spi9.0/spi9.00/nvmem"),
            (std::tuple{"0-8", 9}));
    }

    {
        EXPECT_THROW(util::getFSISPISearchKeys("/sys/some/bad/path"),
                     std::invalid_argument);
    }
}

TEST_F(DeviceCalloutsTest, getCalloutsTest)
{
    std::vector<std::string> systemTypes{"systemA", "systemB"};

    // A really bogus path
    {
        EXPECT_THROW(getCallouts("/bad/path", systemTypes),
                     std::invalid_argument);
    }

    // I2C
    {
        auto callouts = getCallouts(
            "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
            "1e78a340.i2c-bus/i2c-14/14-0072",
            systemTypes);

        std::vector<Callout> expected{
            {"H", "P1-C19", "/chassis/motherboard/cpu0", "core0",
             "I2C: bus: 14 address: 114 dest: proc-0 target"},
            {"M", "P1", "/chassis/motherboard", "", ""}};

        EXPECT_EQ(callouts, expected);

        // Use the bus/address API instead of the device path one
        callouts = getI2CCallouts(14, 0x72, systemTypes);
        EXPECT_EQ(callouts, expected);

        // I2C address not in JSON
        EXPECT_THROW(
            getCallouts(
                "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
                "1e78a340.i2c-bus/i2c-14/14-0099",
                systemTypes),
            std::invalid_argument);

        // A bad JSON entry, missing the location code
        EXPECT_THROW(
            getCallouts(
                "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
                "1e78a340.i2c-bus/i2c-0/0-005a",
                systemTypes),
            std::runtime_error);
    }

    // FSI
    {
        auto callouts = getCallouts(
            "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
            "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
            "slave@01:00/01:01:00:06/sbefifo2-dev0/occ-hwmon.2",
            systemTypes);

        std::vector<Callout> expected{{"H", "P1-C19",
                                       "/chassis/motherboard/cpu0", "core",
                                       "FSI: links: 0-1 dest: proc-0 target"}};

        EXPECT_EQ(callouts, expected);

        // link 9-1 not in JSON
        EXPECT_THROW(
            getCallouts(
                "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                "fsi-master/fsi0/slave@09:00/00:00:00:0a/fsi-master/fsi1/"
                "slave@01:00/01:01:00:06/sbefifo2-dev0/occ-hwmon.2",
                systemTypes),
            std::invalid_argument);
    }

    // FSI-I2C
    {
        auto callouts = getCallouts(
            "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
            "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
            "slave@03:00/01:01:00:03/i2c-207/207-0019",
            systemTypes);

        std::vector<Callout> expected{
            {"H", "P1-C25", "/chassis/motherboard/cpu5", "",
             "FSI-I2C: links: 0-3 bus: 7 addr: 25 dest: proc-5 target"},
            {"M", "P1", "/chassis/motherboard", "", ""},
            {"L", "P2", "/chassis/motherboard/bmc", "", ""}};

        EXPECT_EQ(callouts, expected);

        // Bus 2 not in JSON
        EXPECT_THROW(
            getCallouts(
                "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
                "slave@03:00/01:01:00:03/i2c-202/202-0019",
                systemTypes),
            std::invalid_argument);
    }

    // FSI-SPI
    {
        auto callouts =
            getCallouts("/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                        "fsi-master/fsi0/slave@08:00/00:00:00:04/spi_master/"
                        "spi3/spi3.0/spi3.00/nvmem",
                        systemTypes);

        std::vector<Callout> expected{
            {"H", "P1-C19", "/chassis/motherboard/cpu0", "",
             "FSI-SPI: links: 8 bus: 3 dest: proc-0 target"}};

        EXPECT_EQ(callouts, expected);

        // Bus 7 not in the JSON
        EXPECT_THROW(
            getCallouts("/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
                        "fsi-master/fsi0/slave@08:00/00:00:00:04/spi_master/"
                        "spi7/spi7.0/spi7.00/nvmem",
                        systemTypes),
            std::invalid_argument);
    }
}