/**
 * @file disk_verbs.c
 * @brief Tests the group F verbs: channels, files, and the ones that are refused.
 *
 * These touch the filesystem, so they work in a temporary directory of their own
 * and clean up after themselves. What is asserted is the seam between BASIC and
 * `aksl_f*` -- that a verb reaches the right call with the right arguments --
 * plus, for the five that need a real 1541, that they refuse *by name* rather
 * than doing something plausible and wrong.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <akbasic/error.h>
#include <akbasic/runtime.h>

#include "harness.h"
#include "testutil.h"

/** @brief Run a program to completion in RUN mode, from a string. */
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, harness_start(NULL));
    PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    SUCCEED_RETURN(errctx);
}

/** @brief Read a whole file, for checking what a verb wrote. */
static bool file_contents(const char *name, char *dest, size_t len)
{
    FILE *fp = fopen(name, "rb");
    size_t got = 0;

    if ( fp == NULL ) {
	return false;
    }
    got = fread(dest, 1, len - 1, fp);
    dest[got] = '\0';
    fclose(fp);
    return true;
}

/** @brief A channel written and then read back gives the same lines. */
static void test_channel_round_trip(void)
{
    char contents[256];

    remove("t_chan.txt");
    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_chan.txt\", W\n"
				"20 PRINT #1, \"FIRST\"\n"
				"30 PRINT #1, \"SECOND\"\n"
				"40 DCLOSE 1\n"
				"50 DOPEN 2, \"t_chan.txt\"\n"
				"60 INPUT #2, A$\n"
				"70 INPUT #2, B$\n"
				"80 DCLOSE 2\n"
				"90 PRINT A$\n"
				"100 PRINT B$\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "FIRST\nSECOND\n");
    TEST_REQUIRE(file_contents("t_chan.txt", contents, sizeof(contents)),
		 "the file should exist");
    TEST_REQUIRE_STR(contents, "FIRST\nSECOND\n");
    harness_stop();
    remove("t_chan.txt");
}

/** @brief A numeric variable reads a numeric line, and end of file gives zero. */
static void test_channel_numeric(void)
{
    remove("t_num.txt");
    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_num.txt\", W\n"
				"20 PRINT #1, 42\n"
				"30 DCLOSE 1\n"
				"40 DOPEN 2, \"t_num.txt\"\n"
				"50 INPUT #2, A#\n"
				"60 INPUT #2, B#\n"
				"70 DCLOSE 2\n"
				"80 PRINT A#\n"
				"90 PRINT B#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "42\n0\n");
    harness_stop();
    remove("t_num.txt");
}

/** @brief APPEND adds to a file rather than replacing it. */
static void test_append(void)
{
    char contents[256];

    remove("t_app.txt");
    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_app.txt\", W\n"
				"20 PRINT #1, \"ONE\"\n"
				"30 DCLOSE 1\n"
				"40 APPEND 1, \"t_app.txt\"\n"
				"50 PRINT #1, \"TWO\"\n"
				"60 DCLOSE 1\n"));
    TEST_REQUIRE(file_contents("t_app.txt", contents, sizeof(contents)),
		 "the file should exist");
    TEST_REQUIRE_STR(contents, "ONE\nTWO\n");
    harness_stop();
    remove("t_app.txt");
}

/** @brief Reading a channel opened for writing is refused, and the reverse too. */
static void test_channel_direction(void)
{
    remove("t_dir.txt");
    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_dir.txt\", W\n"
				"20 INPUT #1, A$\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "opened for writing") != NULL,
		 "reading a write channel should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_dir.txt\"\n"
				"20 PRINT #1, \"NO\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "opened for reading") != NULL,
		 "writing a read channel should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
    remove("t_dir.txt");
}

/** @brief An unopened or already-open channel is reported rather than silently reused. */
static void test_channel_state(void)
{
    TEST_REQUIRE_OK(run_program("10 PRINT #3, \"NOWHERE\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is not open") != NULL,
		 "an unopened channel should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    remove("t_two.txt");
    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_two.txt\", W\n"
				"20 DOPEN 1, \"t_two.txt\", W\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "already open") != NULL,
		 "re-opening a channel should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
    remove("t_two.txt");

    TEST_REQUIRE_OK(run_program("10 DOPEN 99, \"x\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..9") != NULL,
		 "channel 99 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief COPY duplicates a file and CONCAT appends one to another. */
static void test_copy_and_concat(void)
{
    char contents[256];

    remove("t_a.txt");
    remove("t_b.txt");
    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_a.txt\", W\n"
				"20 PRINT #1, \"AAA\"\n"
				"30 DCLOSE 1\n"
				"40 COPY \"t_a.txt\", \"t_b.txt\"\n"
				"50 CONCAT \"t_a.txt\", \"t_b.txt\"\n"));
    TEST_REQUIRE(file_contents("t_b.txt", contents, sizeof(contents)),
		 "the copy should exist");
    TEST_REQUIRE_STR(contents, "AAA\nAAA\n");
    harness_stop();
    remove("t_a.txt");
    remove("t_b.txt");
}

/** @brief RENAME moves a file and SCRATCH deletes one. */
static void test_rename_and_scratch(void)
{
    char contents[256];

    remove("t_from.txt");
    remove("t_to.txt");
    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_from.txt\", W\n"
				"20 PRINT #1, \"MOVED\"\n"
				"30 DCLOSE 1\n"
				"40 RENAME \"t_from.txt\", \"t_to.txt\"\n"));
    TEST_REQUIRE(!file_contents("t_from.txt", contents, sizeof(contents)),
		 "the original should be gone");
    TEST_REQUIRE(file_contents("t_to.txt", contents, sizeof(contents)),
		 "the new name should exist");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 SCRATCH \"t_to.txt\"\n"));
    TEST_REQUIRE(!file_contents("t_to.txt", contents, sizeof(contents)),
		 "SCRATCH should have deleted it");
    harness_stop();

    /* Deleting something that is not there is reported rather than ignored. */
    TEST_REQUIRE_OK(run_program("10 SCRATCH \"t_nosuch.txt\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "could not delete") != NULL,
		 "deleting a missing file should be reported, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief BSAVE writes a memory range and BLOAD reads one back. */
static void test_bsave_bload(void)
{
    remove("t_bin.dat");
    TEST_REQUIRE_OK(run_program("10 A$ = \"BINARY\"\n"
				"20 BSAVE \"t_bin.dat\", POINTER(A$), POINTER(A$) + 7\n"
				"30 B$ = \".......\"\n"
				"40 BLOAD \"t_bin.dat\", POINTER(B$), 7\n"
				"50 PRINT B$\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "BINARY\n");
    harness_stop();
    remove("t_bin.dat");

    /* BLOAD needs a length, unlike a C128's; see TODO.md section 5. */
    TEST_REQUIRE_OK(run_program("10 A$ = \"X\"\n20 BLOAD \"t_bin.dat\", POINTER(A$)\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "maximum length") != NULL,
		 "BLOAD without a length should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief The five verbs that need a real drive refuse by name, with the reason.
 *
 * The point is that they do not do something plausible instead. `HEADER` on a
 * filesystem would have to mean "delete everything here", which is a
 * spectacularly bad thing to do to somebody who typed a C128 verb.
 */
static void test_no_drive_verbs(void)
{
    struct { const char *program; const char *expect; } cases[] = {
	{ "10 HEADER \"DISK\"\n",  "formats a disk" },
	{ "10 COLLECT\n",          "block allocation map" },
	{ "10 BACKUP\n",           "duplicates one disk" },
	{ "10 BOOT\n",             "boot sector" },
    };
    size_t i = 0;

    for ( i = 0; i < sizeof(cases) / sizeof(cases[0]); i++ ) {
	TEST_REQUIRE_OK(run_program(cases[i].program));
	TEST_REQUIRE(strstr(HARNESS_OUTPUT, cases[i].expect) != NULL,
		     "expected \"%s\" in the refusal, got \"%s\"",
		     cases[i].expect, HARNESS_OUTPUT);
	TEST_REQUIRE(strstr(HARNESS_OUTPUT, "no disk drive") != NULL,
		     "the refusal should say why, got \"%s\"", HARNESS_OUTPUT);
	harness_stop();
    }

    /*
     * DIRECTORY is refused for a different reason than the five above: not for
     * want of a drive, but because it is unwritten. It used to name the missing
     * libakstdlib wrapper; that wrapper landed, so naming it would be a lie.
     */
    TEST_REQUIRE_OK(run_program("10 DIRECTORY\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "not implemented") != NULL,
		 "DIRECTORY should say it is unwritten, got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") == NULL,
		 "DIRECTORY must not still blame libakstdlib, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief DCLEAR closes every channel, which is the half of it that means something. */
static void test_dclear(void)
{
    remove("t_clear.txt");
    TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_clear.txt\", W\n"
				"20 DCLEAR\n"
				"30 PRINT #1, \"NO\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is not open") != NULL,
		 "DCLEAR should have closed the channel, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
    remove("t_clear.txt");
}

/**
 * @brief DLOAD from a prompt loads the whole program and nothing else.
 *
 * It used to lose the last line. DLOAD scans the file through the runtime's own
 * scanner, so by the time it returns, the tokens of the line that ran it are
 * gone and `hadlinenumber` and `environment->lineno` describe the last line of
 * the *file*. The REPL then carried on parsing the buffer it thought was still
 * its own, decided the leftovers were program text because hadlinenumber was
 * now true, and filed the `DLOAD` command itself under that line number --
 * silently replacing the last line of every program loaded from a prompt.
 *
 * Found while making the examples in docs/ execute: a save/load round trip is
 * the second thing chapter 2 shows a reader.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_dload_keeps_the_last_line(void)
{
    PREPARE_ERROR(errctx);

    remove("t_round.bas");
    TEST_REQUIRE_OK(harness_start("10 PRINT \"FIRST\"\n"
				  "20 PRINT \"SECOND\"\n"
				  "30 PRINT \"THIRD\"\n"
				  "DSAVE \"t_round.bas\"\n"
				  "NEW\n"
				  "DLOAD \"t_round.bas\"\n"
				  "LIST\n"
				  "RUN\n"));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 200));

    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "FIRST\nSECOND\nTHIRD\n") != NULL,
		 "a save/load round trip should run all three lines, got \"%s\"",
		 HARNESS_OUTPUT);
    /*
     * And the command is not in the program. LIST rather than the run output,
     * because a clobbered last line reloads the file and re-runs it, which
     * prints the right text for the wrong reason.
     */
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DLOAD") == NULL,
		 "the DLOAD command should not have been filed as program text, got \"%s\"",
		 HARNESS_OUTPUT);
    harness_stop();
    remove("t_round.bas");
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    test_channel_round_trip();
    test_channel_numeric();
    test_append();
    test_channel_direction();
    test_channel_state();
    test_copy_and_concat();
    test_rename_and_scratch();
    test_bsave_bload();
    test_no_drive_verbs();
    test_dclear();
    IGNORE(test_dload_keeps_the_last_line());
    return akbasic_test_failures;
}
