/**
 * @file sink_tee.c
 * @brief Tests the composing sink that mirrors output to a second sink.
 *
 * This is the piece that lets an AKGL build draw BASIC output in a window and
 * still put it on stdout, without the interpreter carrying a second write. Two
 * stdio sinks over fmemopen buffers stand in for the two halves here -- the
 * composition is what is being tested, not what either half does with the bytes,
 * and using two of the same kind keeps this file free of SDL.
 */

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

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

#include "testutil.h"

static char PRIMARY[1024];
static char MIRROR[1024];

/** @brief A sink whose every entry point fails, for asserting the pair stops. */
static akerr_ErrorContext AKERR_NOIGNORE *broken_write(akbasic_TextSink *self, const char *text)
{
    PREPARE_ERROR(errctx);

    (void)self;
    (void)text;
    FAIL_RETURN(errctx, AKERR_IO, "this sink always fails");
}

static akerr_ErrorContext AKERR_NOIGNORE *broken_clear(akbasic_TextSink *self)
{
    PREPARE_ERROR(errctx);

    (void)self;
    FAIL_RETURN(errctx, AKERR_IO, "this sink always fails");
}

/*
 * A half that has a character grid. The two stdio sinks above have neither
 * `moveto` nor `window`, which is exactly why they cannot show that either one
 * is forwarded -- so this stands in for the akgl sink, without the SDL this file
 * deliberately does not link.
 */
static int MOVETOS = 0;
static int WINDOWS = 0;
static int WINDOW_ARGS[4] = { 0, 0, 0, 0 };
static int GRAPHICS = 0;
static int GRAPHIC_MODE = 0;
static int GRAPHIC_SPLIT = 0;

static akerr_ErrorContext AKERR_NOIGNORE *counting_moveto(akbasic_TextSink *self, int col, int row)
{
    PREPARE_ERROR(errctx);

    (void)self; (void)col; (void)row;
    MOVETOS += 1;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext AKERR_NOIGNORE *counting_window(akbasic_TextSink *self, int left, int top, int right, int bottom)
{
    PREPARE_ERROR(errctx);

    (void)self;
    WINDOWS += 1;
    WINDOW_ARGS[0] = left;
    WINDOW_ARGS[1] = top;
    WINDOW_ARGS[2] = right;
    WINDOW_ARGS[3] = bottom;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext AKERR_NOIGNORE *counting_graphic(akbasic_TextSink *self, int mode, int split)
{
    PREPARE_ERROR(errctx);

    (void)self;
    GRAPHICS += 1;
    GRAPHIC_MODE = mode;
    GRAPHIC_SPLIT = split;
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    akbasic_TextSink primary;
    akbasic_StdioSink primarystate;
    akbasic_TextSink mirror;
    akbasic_StdioSink mirrorstate;
    akbasic_TextSink broken;
    akbasic_TextSink gridded;
    akbasic_TextSink tee;
    akbasic_TeeSink teestate;
    FILE *primaryout = NULL;
    FILE *mirrorout = NULL;
    FILE *in = NULL;
    char line[64];
    bool eof = false;

    TEST_REQUIRE_OK(akbasic_error_register());

    memset(PRIMARY, 0, sizeof(PRIMARY));
    memset(MIRROR, 0, sizeof(MIRROR));
    primaryout = fmemopen(PRIMARY, sizeof(PRIMARY), "w");
    mirrorout = fmemopen(MIRROR, sizeof(MIRROR), "w");
    in = fmemopen((void *)(uintptr_t)"typed\n", 6, "r");
    TEST_REQUIRE(primaryout != NULL && mirrorout != NULL && in != NULL,
		 "could not open the test buffers");
    setvbuf(primaryout, NULL, _IONBF, 0);
    setvbuf(mirrorout, NULL, _IONBF, 0);

    TEST_REQUIRE_OK(akbasic_sink_init_stdio(&primary, &primarystate, primaryout, in));
    TEST_REQUIRE_OK(akbasic_sink_init_stdio(&mirror, &mirrorstate, mirrorout, NULL));

    /* A reader that is neither half is a wiring mistake, and is refused. */
    TEST_REQUIRE_STATUS(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, &broken),
			AKBASIC_ERR_VALUE);
    TEST_REQUIRE_STATUS(akbasic_sink_init_tee(&tee, &teestate, &primary, NULL, NULL),
			AKERR_NULLPOINTER);
    TEST_REQUIRE_STATUS(akbasic_sink_init_tee(NULL, &teestate, &primary, &mirror, NULL),
			AKERR_NULLPOINTER);

    TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, &primary));

    /* Every write reaches both halves, byte for byte, including the newline. */
    TEST_REQUIRE_OK(tee.write(&tee, "AB"));
    TEST_REQUIRE_OK(tee.writeln(&tee, "CD"));
    TEST_REQUIRE_STR(PRIMARY, "ABCD\n");
    TEST_REQUIRE_STR(MIRROR, "ABCD\n");

    /* The error-line double newline survives the composition unchanged. */
    TEST_REQUIRE_OK(tee.writeln(&tee, "? 20 : RUNTIME ERROR something\n"));
    TEST_REQUIRE_STR(MIRROR, "ABCD\n? 20 : RUNTIME ERROR something\n\n");

    /* clear reaches both; the stdio half has no screen, so it just succeeds. */
    TEST_REQUIRE_OK(tee.clear(&tee));

    /* readline comes from the designated reader only. */
    TEST_REQUIRE_OK(tee.readline(&tee, line, sizeof(line), &eof));
    TEST_REQUIRE(!eof, "the reader had a line to give");
    TEST_REQUIRE_STR(line, "typed");
    TEST_REQUIRE_OK(tee.readline(&tee, line, sizeof(line), &eof));
    TEST_REQUIRE(eof, "running off the reader's end must set eof, not raise");

    /* With no reader at all, readline reports end of input rather than raising. */
    TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, NULL));
    eof = false;
    TEST_REQUIRE_OK(tee.readline(&tee, line, sizeof(line), &eof));
    TEST_REQUIRE(eof, "a tee with no reader reports EOF");
    TEST_REQUIRE_STR(line, "");
    TEST_REQUIRE_STATUS(tee.readline(&tee, NULL, sizeof(line), &eof), AKERR_NULLPOINTER);

    /*
     * `moveto` and `window` are offered only when a half can do them, so that a
     * `CHAR` or a `WINDOW` against a stdio-only pair still refuses by name
     * rather than appearing to work. Two stdio halves have neither.
     */
    TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, NULL));
    TEST_REQUIRE(tee.moveto == NULL, "a tee over two grid-less sinks must not offer moveto");
    TEST_REQUIRE(tee.window == NULL, "a tee over two grid-less sinks must not offer window");

    /*
     * And when a half *does* have them, they are forwarded. `window` used to be
     * the one entry point this function never wired, which made WINDOW
     * unreachable in the only build that has a character grid: the standalone
     * frontend runs the interpreter against a tee, so the akgl sink's fully
     * implemented window() answered nothing and the verb refused with "needs a
     * text device with a character grid". TODO.md section 6 item 31.
     */
    gridded.self = &gridded;
    gridded.write = broken_write;
    gridded.writeln = broken_write;
    gridded.readline = NULL;
    gridded.clear = broken_clear;
    gridded.moveto = counting_moveto;
    gridded.window = counting_window;
    gridded.grid = NULL;
    gridded.graphic = counting_graphic;
    MOVETOS = 0;
    WINDOWS = 0;
    GRAPHICS = 0;
    TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &gridded, NULL));
    TEST_REQUIRE(tee.moveto != NULL, "a tee with a gridded half must offer moveto");
    TEST_REQUIRE(tee.window != NULL, "a tee with a gridded half must offer window");
    TEST_REQUIRE(tee.graphic != NULL, "a tee with a graphical half must offer graphic");
    TEST_REQUIRE_OK(tee.moveto(&tee, 3, 4));
    TEST_REQUIRE_OK(tee.window(&tee, 0, 20, 39, 24));
    TEST_REQUIRE_INT(MOVETOS, 1);
    TEST_REQUIRE_INT(WINDOWS, 1);
    TEST_REQUIRE_INT(WINDOW_ARGS[0], 0);
    TEST_REQUIRE_INT(WINDOW_ARGS[1], 20);
    TEST_REQUIRE_INT(WINDOW_ARGS[2], 39);
    TEST_REQUIRE_INT(WINDOW_ARGS[3], 24);
    TEST_REQUIRE_OK(tee.graphic(&tee, 2, 19));
    TEST_REQUIRE_INT(GRAPHICS, 1);
    TEST_REQUIRE_INT(GRAPHIC_MODE, 2);
    TEST_REQUIRE_INT(GRAPHIC_SPLIT, 19);

    /* The gridded half being first is wired the same way round. */
    WINDOWS = 0;
    TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &gridded, &primary, NULL));
    TEST_REQUIRE(tee.window != NULL, "the gridded half may be either one");
    TEST_REQUIRE_OK(tee.window(&tee, 1, 2, 3, 4));
    TEST_REQUIRE_INT(WINDOWS, 1);

    /*
     * A failing half stops the pair rather than being swallowed. A half-written
     * line is worse than a refused one: the golden corpus compares whole files.
     */
    broken.self = &broken;
    broken.write = broken_write;
    broken.writeln = broken_write;
    broken.readline = NULL;
    broken.clear = broken_clear;
    broken.moveto = NULL;
    broken.window = NULL;
    broken.grid = NULL;
    broken.graphic = NULL;
    TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &broken, NULL));
    TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_IO);
    TEST_REQUIRE_STATUS(tee.writeln(&tee, "X"), AKERR_IO);
    TEST_REQUIRE_STATUS(tee.clear(&tee), AKERR_IO);

    TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &broken, &primary, NULL));
    TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_IO);

    /* A sink whose state pointer was never set is caught rather than dereferenced. */
    tee.self = NULL;
    TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_NULLPOINTER);

    fclose(primaryout);
    fclose(mirrorout);
    fclose(in);
    return akbasic_test_failures;
}
