/*
 * Copyright (C) 2018 The Android Open Source Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
#include <chrono>
#include <cstdlib>
#include <fstream>
#include <map>
#include <random>
#include <regex>
#include <set>
#include <thread>
#include <vector>

#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <gtest/gtest.h>

#include "fastboot_driver.h"
#include "tcp.h"
#include "usb.h"

#include "extensions.h"
#include "fixtures.h"
#include "test_utils.h"
#include "transport_sniffer.h"

using namespace std::literals::chrono_literals;

namespace fastboot {

int FastBootTest::MatchFastboot(usb_ifc_info* info, const std::string& local_serial) {
    if (info->ifc_class != 0xff || info->ifc_subclass != 0x42 || info->ifc_protocol != 0x03) {
        return -1;
    }

    cb_scratch = info->device_path;

    // require matching serial number or device path if requested
    // at the command line with the -s option.
    if (!local_serial.empty() && local_serial != info->serial_number &&
        local_serial != info->device_path)
        return -1;
    return 0;
}

bool FastBootTest::IsFastbootOverTcp() {
    return android::base::StartsWith(device_serial, "tcp:");
}

bool FastBootTest::UsbStillAvailible() {
    if (IsFastbootOverTcp()) return true;

    // For some reason someone decided to prefix the path with "usb:"
    std::string prefix("usb:");
    if (std::equal(prefix.begin(), prefix.end(), device_path.begin())) {
        std::string fname(device_path.begin() + prefix.size(), device_path.end());
        std::string real_path =
                android::base::StringPrintf("/sys/bus/usb/devices/%s/serial", fname.c_str());
        std::ifstream f(real_path.c_str());
        return f.good();
    }
    exit(-1);  // This should never happen
    return true;
}

bool FastBootTest::UserSpaceFastboot() {
    std::string value;
    fb->GetVar("is-userspace", &value);
    return value == "yes";
}

RetCode FastBootTest::DownloadCommand(uint32_t size, std::string* response,
                                      std::vector<std::string>* info) {
    return fb->DownloadCommand(size, response, info);
}

RetCode FastBootTest::SendBuffer(const std::vector<char>& buf) {
    return fb->SendBuffer(buf);
}

RetCode FastBootTest::HandleResponse(std::string* response, std::vector<std::string>* info,
                                     int* dsize) {
    return fb->HandleResponse(response, info, dsize);
}

void FastBootTest::SetUp() {
    if (device_path != "") {               // make sure the device is still connected
        ASSERT_TRUE(UsbStillAvailible());  // The device disconnected
    }

    if (IsFastbootOverTcp()) {
        ConnectTcpFastbootDevice();
    } else {
        const auto matcher = [](usb_ifc_info* info) -> int {
            return MatchFastboot(info, device_serial);
        };
        for (int i = 0; i < MAX_USB_TRIES && !transport; i++) {
            std::unique_ptr<UsbTransport> usb(usb_open(matcher, USB_TIMEOUT));
            if (usb)
                transport = std::unique_ptr<TransportSniffer>(
                        new TransportSniffer(std::move(usb), serial_port));
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }

    ASSERT_TRUE(transport);  // no nullptr

    if (device_path == "") {  // We set it the first time, then make sure it never changes
        device_path = cb_scratch;
    } else {
        ASSERT_EQ(device_path, cb_scratch);  // The path can not change
    }
    fb = std::unique_ptr<FastBootDriver>(new FastBootDriver(transport.get(), {}, true));
    // No error checking since non-A/B devices may not support the command
    fb->GetVar("current-slot", &initial_slot);
}

void FastBootTest::TearDown() {
    EXPECT_TRUE(UsbStillAvailible()) << USB_PORT_GONE;
    // No error checking since non-A/B devices may not support the command
    fb->SetActive(initial_slot);

    TearDownSerial();

    fb.reset();

    if (transport) {
        transport.reset();
    }

    ASSERT_TRUE(UsbStillAvailible()) << USB_PORT_GONE;
}

// TODO, this should eventually be piped to a file instead of stdout
void FastBootTest::TearDownSerial() {
    if (IsFastbootOverTcp()) return;

    if (!transport) return;
    // One last read from serial
    transport->ProcessSerial();
    if (HasFailure()) {
        // TODO, print commands leading up
        printf("<<<<<<<< TRACE BEGIN >>>>>>>>>\n");
        printf("%s", transport->CreateTrace().c_str());
        printf("<<<<<<<< TRACE END >>>>>>>>>\n");
        // std::vector<std::pair<const TransferType, const std::vector<char>>>  prev =
        // transport->Transfers();
    }
}

void FastBootTest::ConnectTcpFastbootDevice() {
    for (int i = 0; i < MAX_TCP_TRIES && !transport; i++) {
        std::string error;
        std::unique_ptr<Transport> tcp(
                tcp::Connect(device_serial.substr(4), tcp::kDefaultPort, &error).release());
        if (tcp)
            transport = std::unique_ptr<TransportSniffer>(new TransportSniffer(std::move(tcp), 0));
        if (transport != nullptr) break;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

void FastBootTest::ReconnectFastbootDevice() {
    fb.reset();
    transport.reset();

    if (IsFastbootOverTcp()) {
        ConnectTcpFastbootDevice();
        device_path = cb_scratch;
        fb = std::unique_ptr<FastBootDriver>(new FastBootDriver(transport.get(), {}, true));
        return;
    }

    while (UsbStillAvailible())
        ;
    printf("WAITING FOR DEVICE\n");
    // Need to wait for device
    const auto matcher = [](usb_ifc_info* info) -> int {
        return MatchFastboot(info, device_serial);
    };
    while (!transport) {
        std::unique_ptr<UsbTransport> usb(usb_open(matcher, USB_TIMEOUT));
        if (usb) {
            transport = std::unique_ptr<TransportSniffer>(
                    new TransportSniffer(std::move(usb), serial_port));
        }
        std::this_thread::sleep_for(1s);
    }
    device_path = cb_scratch;
    fb = std::unique_ptr<FastBootDriver>(new FastBootDriver(transport.get(), {}, true));
}

void FastBootTest::SetLockState(bool unlock, bool assert_change) {
    if (!fb) {
        return;
    }

    // User space fastboot implementations are not allowed to communicate to
    // secure hardware and hence cannot lock/unlock the device.
    if (UserSpaceFastboot()) {
        return;
    }

    std::string resp;
    std::vector<std::string> info;
    // To avoid risk of bricking device, make sure unlock ability is set to 1
    ASSERT_EQ(fb->RawCommand("flashing get_unlock_ability", &resp, &info), SUCCESS)
            << "'flashing get_unlock_ability' failed";

    // There are two ways this can be reported, through info or the actual response
    if (!resp.empty()) {  // must be in the info response
        ASSERT_EQ(resp.back(), '1')
                << "Unlock ability must be set to 1 to avoid bricking device, see "
                   "'https://source.android.com/devices/bootloader/unlock-trusty'";
    } else {
        ASSERT_FALSE(info.empty()) << "'flashing get_unlock_ability' returned empty response";
        ASSERT_FALSE(info.back().empty()) << "Expected non-empty info response";
        ASSERT_EQ(info.back().back(), '1')
                << "Unlock ability must be set to 1 to avoid bricking device, see "
                   "'https://source.android.com/devices/bootloader/unlock-trusty'";
    }

    EXPECT_EQ(fb->GetVar("unlocked", &resp), SUCCESS) << "getvar:unlocked failed";
    ASSERT_TRUE(resp == "no" || resp == "yes")
            << "getvar:unlocked response was not 'no' or 'yes': " + resp;

    if ((unlock && resp == "no") || (!unlock && resp == "yes")) {
        std::string cmd = unlock ? "unlock" : "lock";
        ASSERT_EQ(fb->RawCommand("flashing " + cmd, &resp), SUCCESS)
                << "Attempting to change locked state, but 'flashing" + cmd + "' command failed";
        printf("PLEASE RESPOND TO PROMPT FOR '%sing' BOOTLOADER ON DEVICE\n", cmd.c_str());
        ReconnectFastbootDevice();
        if (assert_change) {
            ASSERT_EQ(fb->GetVar("unlocked", &resp), SUCCESS) << "getvar:unlocked failed";
            ASSERT_EQ(resp, unlock ? "yes" : "no")
                    << "getvar:unlocked response was not 'no' or 'yes': " + resp;
        }
        printf("SUCCESS\n");
    }
}

std::string FastBootTest::device_path = "";
std::string FastBootTest::cb_scratch = "";
std::string FastBootTest::initial_slot = "";
int FastBootTest::serial_port = 0;
std::string FastBootTest::device_serial = "";

template <bool UNLOCKED>
void ModeTest<UNLOCKED>::SetUp() {
    ASSERT_NO_FATAL_FAILURE(FastBootTest::SetUp());
    ASSERT_NO_FATAL_FAILURE(SetLockState(UNLOCKED));
}
// Need to instatiate it, so linker can find it later
template class ModeTest<true>;
template class ModeTest<false>;

void Fuzz::TearDown() {
    ASSERT_TRUE(UsbStillAvailible()) << USB_PORT_GONE;

    TearDownSerial();

    std::string tmp;
    if (fb->GetVar("product", &tmp) != SUCCESS) {
        printf("DEVICE UNRESPONSE, attempting to recover...");
        transport->Reset();
        printf("issued USB reset...");

        if (fb->GetVar("product", &tmp) != SUCCESS) {
            printf("FAIL\n");
            exit(-1);
        }
        printf("SUCCESS!\n");
    }

    if (transport) {
        transport.reset();
    }

    ASSERT_TRUE(UsbStillAvailible()) << USB_PORT_GONE;
}

template <bool UNLOCKED>
void ExtensionsPartition<UNLOCKED>::SetUp() {
    ASSERT_NO_FATAL_FAILURE(FastBootTest::SetUp());
    ASSERT_NO_FATAL_FAILURE(SetLockState(UNLOCKED));

    if (!fb) {
        return;
    }
    const std::string name = GetParam().first;

    std::string var;
    ASSERT_EQ(fb->GetVar("slot-count", &var), SUCCESS) << "Getting slot count failed";
    int32_t num_slots = strtol(var.c_str(), nullptr, 10);
    real_parts = GeneratePartitionNames(name, GetParam().second.slots ? num_slots : 0);

    ASSERT_EQ(fb->GetVar("partition-size:" + real_parts.front(), &var), SUCCESS)
            << "Getting partition size failed";
    part_size = strtoll(var.c_str(), nullptr, 16);
    ASSERT_GT(part_size, 0) << "Partition size reported was invalid";

    ASSERT_EQ(fb->GetVar("max-download-size", &var), SUCCESS) << "Getting max download size failed";
    max_dl = strtoll(var.c_str(), nullptr, 16);
    ASSERT_GT(max_dl, 0) << "Max download size reported was invalid";

    max_flash = std::min(part_size, max_dl);
}
template class ExtensionsPartition<true>;
template class ExtensionsPartition<false>;

}  // end namespace fastboot
