/**
 * @file scanner_tokens.c
 * @brief Tests the line tokenizer.
 */

#include "harness.h"

static akbasic_Environment *env(void)
{
    return HARNESS_RUNTIME.environment;
}

/* Scan a line and assert the token type at a given index. */
static void expect_token(int index, akbasic_TokenType want, const char *lexeme)
{
    TEST_REQUIRE(index < env()->nexttoken,
		 "expected a token at index %d, only %d were produced",
		 index, env()->nexttoken);
    if ( index >= env()->nexttoken ) {
	return;
    }
    TEST_REQUIRE_INT(env()->tokens[index].tokentype, want);
    if ( lexeme != NULL ) {
	TEST_REQUIRE_STR(env()->tokens[index].lexeme, lexeme);
    }
}

int main(void)
{
    char rewritten[AKBASIC_MAX_LINE_LENGTH];

    TEST_REQUIRE_OK(harness_start(NULL));

    /* Single-character operators. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "+-*/^(),", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 8);
    expect_token(0, AKBASIC_TOK_PLUS, "+");
    expect_token(1, AKBASIC_TOK_MINUS, "-");
    expect_token(2, AKBASIC_TOK_STAR, "*");
    expect_token(3, AKBASIC_TOK_LEFT_SLASH, "/");
    expect_token(4, AKBASIC_TOK_CARAT, "^");
    expect_token(5, AKBASIC_TOK_LEFT_PAREN, "(");
    expect_token(6, AKBASIC_TOK_RIGHT_PAREN, ")");
    expect_token(7, AKBASIC_TOK_COMMA, ",");

    /* Two-character comparisons, and the one-character fallbacks. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "<= <> >= < > == = ", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 7);
    expect_token(0, AKBASIC_TOK_LESS_THAN_EQUAL, NULL);
    expect_token(1, AKBASIC_TOK_NOT_EQUAL, NULL);
    expect_token(2, AKBASIC_TOK_GREATER_THAN_EQUAL, NULL);
    expect_token(3, AKBASIC_TOK_LESS_THAN, NULL);
    expect_token(4, AKBASIC_TOK_GREATER_THAN, NULL);
    expect_token(5, AKBASIC_TOK_EQUAL, NULL);
    expect_token(6, AKBASIC_TOK_ASSIGNMENT, NULL);

    /*
     * An operator in the final column survives, with no trailing space to help
     * it. matchNextChar cannot peek past the end of the line, and the reference
     * returns from that without setting a token type (basicscanner.go:272), so
     * `A# =` produced one token and the parse error that followed pointed
     * somewhere else. TODO.md section 6 item 14; this is its regression test.
     */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# =", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 2);
    expect_token(0, AKBASIC_TOK_IDENTIFIER_INT, "A#");
    expect_token(1, AKBASIC_TOK_ASSIGNMENT, "=");

    /* The same for the other two, each alone in the final column. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# <", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 2);
    expect_token(1, AKBASIC_TOK_LESS_THAN, "<");
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# >", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 2);
    expect_token(1, AKBASIC_TOK_GREATER_THAN, ">");

    /* Literals. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 1.5", NULL, 0));
    expect_token(0, AKBASIC_TOK_COMMAND, "PRINT");
    expect_token(1, AKBASIC_TOK_LITERAL_FLOAT, "1.5");

    /*
     * A hex literal survives the scanner whole. The reference lets 'x' through
     * but stops at the hex digits after it (basicscanner.go:318), so "0xff"
     * lexed as "0x" plus an identifier "ff", the parser's base-16 branch was
     * unreachable and the README's hex support did not exist. TODO.md section 6
     * item 15; this is its regression test.
     */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xff", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 2);
    expect_token(1, AKBASIC_TOK_LITERAL_INT, "0xff");

    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xDEAD", NULL, 0));
    expect_token(1, AKBASIC_TOK_LITERAL_INT, "0xDEAD");

    /* And the hex run ends where it should, rather than eating what follows. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0x10 + 1", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 4);
    expect_token(1, AKBASIC_TOK_LITERAL_INT, "0x10");
    expect_token(2, AKBASIC_TOK_PLUS, "+");
    expect_token(3, AKBASIC_TOK_LITERAL_INT, "1");

    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT \"HELLO\"", NULL, 0));
    expect_token(1, AKBASIC_TOK_LITERAL_STRING, "HELLO");

    /* An empty string literal really is empty, not a stray quote. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT \"\"", NULL, 0));
    expect_token(1, AKBASIC_TOK_LITERAL_STRING, "");

    /* Identifier suffixes pick the token type. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# B% C$ D", NULL, 0));
    expect_token(0, AKBASIC_TOK_IDENTIFIER_INT, "A#");
    expect_token(1, AKBASIC_TOK_IDENTIFIER_FLOAT, "B%");
    expect_token(2, AKBASIC_TOK_IDENTIFIER_STRING, "C$");
    expect_token(3, AKBASIC_TOK_IDENTIFIER, "D");

    /* Verbs are case-insensitive; a variable is not a verb. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "print goto", NULL, 0));
    expect_token(0, AKBASIC_TOK_COMMAND, "print");
    expect_token(1, AKBASIC_TOK_COMMAND, "goto");

    /* Reserved words get their own token types, not COMMAND. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# AND B# OR NOT C#", NULL, 0));
    expect_token(1, AKBASIC_TOK_AND, NULL);
    expect_token(3, AKBASIC_TOK_OR, NULL);
    expect_token(4, AKBASIC_TOK_NOT, NULL);

    /*
     * The line-number rule: an integer in token position 0 is consumed, not
     * emitted, and the line is rewritten to what follows it with leading spaces
     * stripped. The REPL stores the rewritten line as the program text, so this
     * is load-bearing rather than cosmetic.
     */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "10 PRINT 1", rewritten, sizeof(rewritten)));
    TEST_REQUIRE_STR(rewritten, "PRINT 1");
    TEST_REQUIRE_INT(env()->lineno, 10);
    expect_token(0, AKBASIC_TOK_COMMAND, "PRINT");
    expect_token(1, AKBASIC_TOK_LITERAL_INT, "1");

    /* An integer that is *not* in position 0 stays a literal. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 10", NULL, 0));
    expect_token(1, AKBASIC_TOK_LITERAL_INT, "10");

    /* REM ends the line: nothing after it becomes a token. */
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "REM PRINT \"NOT SEEN\"", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 0);

    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "10 REM A COMMENT", NULL, 0));
    TEST_REQUIRE_INT(env()->nexttoken, 0);
    TEST_REQUIRE_INT(env()->lineno, 10);

    /*
     * A verb name with a type suffix stuck on it is refused as a variable name.
     * The reference searches the keyword tables with the suffix still attached
     * (basicscanner.go:349), so "PRINT$" misses every table, its own "Reserved
     * word in variable name" diagnostic is dead code, and `PRINT$ = 1` is
     * quietly accepted. TODO.md section 6 item 16; this is its regression test.
     */
    HARNESS_RUNTIME.hasError = false;
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT$ = 1", NULL, 0));
    TEST_REQUIRE(HARNESS_RUNTIME.hasError, "PRINT$ must be refused as a variable name");

    /* The same for a function name, and for the other two suffixes. */
    HARNESS_RUNTIME.hasError = false;
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "LEN# = 1", NULL, 0));
    TEST_REQUIRE(HARNESS_RUNTIME.hasError, "LEN# must be refused as a variable name");
    HARNESS_RUNTIME.hasError = false;
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "GOTO% = 1", NULL, 0));
    TEST_REQUIRE(HARNESS_RUNTIME.hasError, "GOTO%% must be refused as a variable name");

    /*
     * And an ordinary name that merely *contains* a verb is still fine, which is
     * what says the check compares the whole base name rather than a prefix.
     */
    HARNESS_RUNTIME.hasError = false;
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINTER$ = 1", NULL, 0));
    TEST_REQUIRE(!HARNESS_RUNTIME.hasError, "PRINTER$ is not a reserved word");
    expect_token(0, AKBASIC_TOK_IDENTIFIER_STRING, "PRINTER$");

    /* An unknown character is diagnosed. */
    HARNESS_RUNTIME.hasError = false;
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT ~", NULL, 0));
    TEST_REQUIRE(HARNESS_RUNTIME.hasError, "an unknown token must be diagnosed");

    /* A malformed number is diagnosed rather than silently becoming zero. */
    HARNESS_RUNTIME.hasError = false;
    TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "1x2 PRINT 1", NULL, 0));
    TEST_REQUIRE(HARNESS_RUNTIME.hasError,
		 "a malformed line number must be diagnosed, not converted to 0");
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "INTEGER CONVERSION ON '1x2'") != NULL,
		 "expected the reference's conversion message, got: %s", HARNESS_OUTPUT);

    TEST_REQUIRE_STATUS(akbasic_scanner_scan(&HARNESS_RUNTIME, NULL, NULL, 0), AKERR_NULLPOINTER);

    harness_stop();
    return akbasic_test_failures;
}
