/*
 * QTests for NPCM7xx SD-3.0 / MMC-4.51 Host Controller
 *
 * Copyright (c) 2022 Google LLC
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 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 General Public License
 * for more details.
 */

#include "qemu/osdep.h"
#include "hw/sd/npcm7xx_sdhci.h"

#include "libqtest.h"
#include "libqtest-single.h"
#include "libqos/sdhci-cmd.h"

#define NPCM7XX_REG_SIZE 0x100
#define NPCM7XX_MMC_BA 0xF0842000
#define NPCM7XX_BLK_SIZE 512
#define NPCM7XX_TEST_IMAGE_SIZE (1 << 20)

char *sd_path;

static QTestState *setup_sd_card(void)
{
    QTestState *qts = qtest_initf(
        "-machine kudo-bmc "
        "-device sd-card,drive=drive0 "
        "-drive id=drive0,if=none,file=%s,format=raw,auto-read-only=off",
        sd_path);

    qtest_writew(qts, NPCM7XX_MMC_BA + SDHC_SWRST, SDHC_RESET_ALL);
    qtest_writew(qts, NPCM7XX_MMC_BA + SDHC_CLKCON,
                 SDHC_CLOCK_SDCLK_EN | SDHC_CLOCK_INT_STABLE |
                     SDHC_CLOCK_INT_EN);
    sdhci_cmd_regs(qts, NPCM7XX_MMC_BA, 0, 0, 0, 0, SDHC_APP_CMD);
    sdhci_cmd_regs(qts, NPCM7XX_MMC_BA, 0, 0, 0x41200000, 0, (41 << 8));
    sdhci_cmd_regs(qts, NPCM7XX_MMC_BA, 0, 0, 0, 0, SDHC_ALL_SEND_CID);
    sdhci_cmd_regs(qts, NPCM7XX_MMC_BA, 0, 0, 0, 0, SDHC_SEND_RELATIVE_ADDR);
    sdhci_cmd_regs(qts, NPCM7XX_MMC_BA, 0, 0, 0x45670000, 0,
                   SDHC_SELECT_DESELECT_CARD);

    return qts;
}

static void write_sdread(QTestState *qts, const char *msg)
{
    int fd, ret;
    size_t len = strlen(msg);
    char *rmsg = g_malloc(len);

    /* write message to sd */
    fd = open(sd_path, O_WRONLY);
    g_assert(fd >= 0);
    ret = write(fd, msg, len);
    close(fd);
    g_assert(ret == len);

    /* read message using sdhci */
    ret = sdhci_read_cmd(qts, NPCM7XX_MMC_BA, rmsg, len);
    g_assert(ret == len);
    g_assert(!memcmp(rmsg, msg, len));

    g_free(rmsg);
}

/* Check MMC can read values from sd */
static void test_read_sd(void)
{
    QTestState *qts = setup_sd_card();

    write_sdread(qts, "hello world");
    write_sdread(qts, "goodbye");

    qtest_quit(qts);
}

static void sdwrite_read(QTestState *qts, const char *msg)
{
    int fd, ret;
    size_t len = strlen(msg);
    char *rmsg = g_malloc(len);

    /* write message using sdhci */
    sdhci_write_cmd(qts, NPCM7XX_MMC_BA, msg, len, NPCM7XX_BLK_SIZE);

    /* read message from sd */
    fd = open(sd_path, O_RDONLY);
    g_assert(fd >= 0);
    ret = read(fd, rmsg, len);
    close(fd);
    g_assert(ret == len);

    g_assert(!memcmp(rmsg, msg, len));

    g_free(rmsg);
}

/* Check MMC can write values to sd */
static void test_write_sd(void)
{
    QTestState *qts = setup_sd_card();

    sdwrite_read(qts, "hello world");
    sdwrite_read(qts, "goodbye");

    qtest_quit(qts);
}

/* Check SDHCI has correct default values. */
static void test_reset(void)
{
    QTestState *qts = qtest_init("-machine kudo-bmc");
    uint64_t addr = NPCM7XX_MMC_BA;
    uint64_t end_addr = addr + NPCM7XX_REG_SIZE;
    uint16_t prstvals_resets[] = {NPCM7XX_PRSTVALS_0_RESET,
                                  NPCM7XX_PRSTVALS_1_RESET,
                                  0,
                                  NPCM7XX_PRSTVALS_3_RESET,
                                  0,
                                  0};
    int i;
    uint32_t mask;

    while (addr < end_addr) {
        switch (addr - NPCM7XX_MMC_BA) {
        case SDHC_PRNSTS:
            /*
             * ignores bits 20 to 24: they are changed when reading registers
             */
            mask = 0x1f00000;
            g_assert_cmphex(qtest_readl(qts, addr) | mask, ==,
                            NPCM7XX_PRSNTS_RESET | mask);
            addr += 4;
            break;
        case SDHC_BLKGAP:
            g_assert_cmphex(qtest_readb(qts, addr), ==, NPCM7XX_BLKGAP_RESET);
            addr += 1;
            break;
        case SDHC_CAPAB:
            g_assert_cmphex(qtest_readq(qts, addr), ==, NPCM7XX_CAPAB_RESET);
            addr += 8;
            break;
        case SDHC_MAXCURR:
            g_assert_cmphex(qtest_readq(qts, addr), ==, NPCM7XX_MAXCURR_RESET);
            addr += 8;
            break;
        case SDHC_HCVER:
            g_assert_cmphex(qtest_readw(qts, addr), ==, NPCM7XX_HCVER_RESET);
            addr += 2;
            break;
        case NPCM7XX_PRSTVALS:
            for (i = 0; i < NPCM7XX_PRSTVALS_SIZE; ++i) {
                g_assert_cmphex(qtest_readw(qts, addr + 2 * i), ==,
                                prstvals_resets[i]);
            }
            addr += NPCM7XX_PRSTVALS_SIZE * 2;
            break;
        default:
            g_assert_cmphex(qtest_readb(qts, addr), ==, 0);
            addr += 1;
        }
    }

    qtest_quit(qts);
}

static void drive_destroy(void)
{
    unlink(sd_path);
    g_free(sd_path);
}

static void drive_create(void)
{
    int fd, ret;
    GError *error = NULL;

    /* Create a temporary raw image */
    fd = g_file_open_tmp("sdhci_XXXXXX", &sd_path, &error);
    if (fd == -1) {
        fprintf(stderr, "unable to create sdhci file: %s\n", error->message);
        g_error_free(error);
    }
    g_assert(sd_path != NULL);

    ret = ftruncate(fd, NPCM7XX_TEST_IMAGE_SIZE);
    g_assert_cmpint(ret, ==, 0);
    g_message("%s", sd_path);
    close(fd);
}

int main(int argc, char **argv)
{
    int ret;

    drive_create();

    g_test_init(&argc, &argv, NULL);

    qtest_add_func("npcm7xx_sdhci/reset", test_reset);
    qtest_add_func("npcm7xx_sdhci/write_sd", test_write_sd);
    qtest_add_func("npcm7xx_sdhci/read_sd", test_read_sd);

    ret = g_test_run();
    drive_destroy();
    return ret;
}