/*
    pathwatch_test.c — unit test driver for the J1 path-change heuristic.

    Standalone, no root, no network, no ncurses:

        cc -Wall -Wextra -o tests/pathwatch_test tests/pathwatch_test.c lft_pathwatch.c
        ./tests/pathwatch_test        (exit 0 = pass)

    Scenarios mirror the design doc test plan (Tier 1), one function each.
*/
#include <stdio.h>
#include <string.h>
#include "../lft_pathwatch.h"

static int checks = 0, failures = 0;

#define CHECK(cond) do {                                                     \
        checks++;                                                            \
        if (!(cond)) {                                                       \
            failures++;                                                      \
            printf("    FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond);       \
        }                                                                    \
    } while (0)

#define SCENARIO(name) printf("  %s\n", name)

/* Convenience: fresh state with confirmed IP preloaded and noted. */
static void setup(watch_pathwatch_t *pw, char *conf, const char *ip)
{
    memset(pw, 0, sizeof(*pw));
    conf[0] = '\0';
    if (ip) {
        snprintf(conf, PW_IPLEN, "%s", ip);
        pw_note_ip(pw, ip);
    }
}

static int in_set(const watch_pathwatch_t *pw, const char *ip)
{
    int i;
    for (i = 0; i < pw->n_known; i++)
        if (strcmp(pw->known_ips[i], ip) == 0)
            return 1;
    return 0;
}

/* 1. Learning: unarmed observations are discovery; sets populate quietly. */
static void t01_learning(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    SCENARIO("01 learning phase = discovery");
    setup(&pw, conf, NULL);

    CHECK(pw_observe(&pw, conf, "10.0.0.1", 0, NULL) == PW_DISCOVERY);
    CHECK(strcmp(conf, "10.0.0.1") == 0);
    /* pre-arm change is still discovery, not pending/alert */
    CHECK(pw_observe(&pw, conf, "10.0.0.2", 0, NULL) == PW_DISCOVERY);
    CHECK(strcmp(conf, "10.0.0.2") == 0);
    CHECK(in_set(&pw, "10.0.0.1"));
    CHECK(in_set(&pw, "10.0.0.2"));
    CHECK(pw.pending_ip[0] == '\0');
}

/* 2. Arming latch truth table. */
static void t02_arming(void)
{
    SCENARIO("02 arming latch");
    CHECK(pw_update_armed(0, 0, 1) == 0);
    CHECK(pw_update_armed(0, 2, 7) == 0);
    CHECK(pw_update_armed(0, 3, 3) == 1);   /* stable path arms   */
    CHECK(pw_update_armed(0, 0, 8) == 1);   /* cycle-8 fallback   */
    CHECK(pw_update_armed(1, 0, 2) == 1);   /* never disarms      */
}

/* 3. Stable path: same IP forever, no pending. */
static void t03_stable(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    int i;
    SCENARIO("03 stable path");
    setup(&pw, conf, "192.0.2.1");
    for (i = 0; i < 5; i++)
        CHECK(pw_observe(&pw, conf, "192.0.2.1", 1, NULL) == PW_STABLE);
    CHECK(strcmp(conf, "192.0.2.1") == 0);
}

/* 4. One-cycle flap: X, Y, X -> PENDING then FLAP; confirmed never moves. */
static void t04_flap(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN], was[PW_IPLEN] = "";
    SCENARIO("04 one-cycle flap");
    setup(&pw, conf, "192.0.2.1");

    CHECK(pw_observe(&pw, conf, "192.0.2.9", 1, was) == PW_PENDING);
    CHECK(strcmp(conf, "192.0.2.1") == 0);          /* not accepted yet */
    CHECK(pw_observe(&pw, conf, "192.0.2.1", 1, was) == PW_FLAP);
    CHECK(strcmp(was, "192.0.2.9") == 0);           /* flap_was reports Y */
    CHECK(strcmp(conf, "192.0.2.1") == 0);
    CHECK(pw.pending_ip[0] == '\0');                /* pending cleared */
}

/* 5. Genuine reroute: X, Y, Y -> PENDING then CONFIRMED; set reset to {Y}. */
static void t05_reroute(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    SCENARIO("05 genuine reroute");
    setup(&pw, conf, "192.0.2.1");

    CHECK(pw_observe(&pw, conf, "192.0.2.9", 1, NULL) == PW_PENDING);
    CHECK(pw_observe(&pw, conf, "192.0.2.9", 1, NULL) == PW_CONFIRMED);
    CHECK(strcmp(conf, "192.0.2.9") == 0);
    CHECK(pw.n_known == 1);                          /* set reset ... */
    CHECK(in_set(&pw, "192.0.2.9"));                 /* ... to {Y}    */
    CHECK(pw.pending_ip[0] == '\0');
}

/* 6. Route restore: after confirmed change, old IP alerts again. */
static void t06_route_restore(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    SCENARIO("06 route restore alerts (set was reset)");
    setup(&pw, conf, "192.0.2.1");
    pw_observe(&pw, conf, "192.0.2.9", 1, NULL);
    pw_observe(&pw, conf, "192.0.2.9", 1, NULL);     /* confirmed Y */

    CHECK(pw_observe(&pw, conf, "192.0.2.1", 1, NULL) == PW_PENDING);
    CHECK(pw_observe(&pw, conf, "192.0.2.1", 1, NULL) == PW_CONFIRMED);
    CHECK(strcmp(conf, "192.0.2.1") == 0);
}

/* 7. ECMP alternation: PENDING, FLAP, then ECMP_SHIFT on every transition. */
static void t07_ecmp_alternation(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    int i;
    SCENARIO("07 ECMP alternation");
    setup(&pw, conf, "203.0.113.1");

    CHECK(pw_observe(&pw, conf, "203.0.113.2", 1, NULL) == PW_PENDING);
    CHECK(pw_observe(&pw, conf, "203.0.113.1", 1, NULL) == PW_FLAP);
    /* .2 was noted at first sighting -> membership now classifies it */
    CHECK(pw_observe(&pw, conf, "203.0.113.2", 1, NULL) == PW_ECMP_SHIFT);
    CHECK(strcmp(conf, "203.0.113.2") == 0);
    CHECK(pw_observe(&pw, conf, "203.0.113.1", 1, NULL) == PW_ECMP_SHIFT);

    /* counter drives log suppression in the caller */
    CHECK(pw.ecmp_logged == 2);
    for (i = 0; i < 4; i++) {
        pw_observe(&pw, conf, "203.0.113.2", 1, NULL);
        pw_observe(&pw, conf, "203.0.113.1", 1, NULL);
    }
    CHECK(pw.ecmp_logged == 10);
    CHECK(pw.ecmp_logged > PW_ECMP_LOG_MAX);
}

/* 8. alt_hosts feeding: pre-noted IP shifts immediately, no hysteresis. */
static void t08_alt_hosts(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    SCENARIO("08 alt_hosts pre-noted -> immediate ECMP shift");
    setup(&pw, conf, "203.0.113.1");
    pw_note_ip(&pw, "203.0.113.2");                  /* seen as alt in-cycle */

    CHECK(pw_observe(&pw, conf, "203.0.113.2", 1, NULL) == PW_ECMP_SHIFT);
    CHECK(strcmp(conf, "203.0.113.2") == 0);
}

/* 9. Cloaked gaps: pending survives '*'; '*' alone never alerts. */
static void t09_cloaked(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    SCENARIO("09 cloaked-gap hysteresis");
    setup(&pw, conf, "192.0.2.1");

    CHECK(pw_observe(&pw, conf, "192.0.2.9", 1, NULL) == PW_PENDING);
    CHECK(pw_observe(&pw, conf, "*", 1, NULL) == PW_IGNORE);
    CHECK(strcmp(pw.pending_ip, "192.0.2.9") == 0);  /* preserved     */
    CHECK(pw.pending_cycles == 1);                   /* not advanced  */
    CHECK(pw_observe(&pw, conf, "192.0.2.9", 1, NULL) == PW_CONFIRMED);

    /* '*' <-> IP transitions alone: no alerts either direction */
    setup(&pw, conf, "192.0.2.1");
    CHECK(pw_observe(&pw, conf, "*", 1, NULL) == PW_IGNORE);
    CHECK(pw_observe(&pw, conf, "", 1, NULL) == PW_IGNORE);
    CHECK(pw_observe(&pw, conf, "192.0.2.1", 1, NULL) == PW_STABLE);
}

/* 10. Third-IP churn: X, Y, Z, Z -> pending rebinds, Z confirms. */
static void t10_churn(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    SCENARIO("10 third-IP churn");
    setup(&pw, conf, "192.0.2.1");

    CHECK(pw_observe(&pw, conf, "192.0.2.8", 1, NULL) == PW_PENDING);
    CHECK(pw_observe(&pw, conf, "192.0.2.9", 1, NULL) == PW_PENDING);
    CHECK(strcmp(pw.pending_ip, "192.0.2.9") == 0);  /* rebound to Z */
    CHECK(pw_observe(&pw, conf, "192.0.2.9", 1, NULL) == PW_CONFIRMED);
    CHECK(strcmp(conf, "192.0.2.9") == 0);
    CHECK(pw.n_known == 1);                          /* Y evicted by reset */
}

/* 11. Set overflow: caps at PW_KNOWN_MAX; hysteresis still confirms. */
static void t11_overflow(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    char ip[PW_IPLEN];
    int i;
    SCENARIO("11 known-set overflow");
    setup(&pw, conf, "10.9.9.1");

    for (i = 0; i < 10; i++) {
        snprintf(ip, sizeof(ip), "10.1.1.%d", i + 1);
        pw_note_ip(&pw, ip);
    }
    CHECK(pw.n_known == PW_KNOWN_MAX);
    pw_note_ip(&pw, "10.1.1.1");                     /* dup: no change */
    CHECK(pw.n_known == PW_KNOWN_MAX);

    /* 9th-IP change still alerts through hysteresis */
    CHECK(pw_observe(&pw, conf, "10.2.2.2", 1, NULL) == PW_PENDING);
    CHECK(pw_observe(&pw, conf, "10.2.2.2", 1, NULL) == PW_CONFIRMED);
    CHECK(strcmp(conf, "10.2.2.2") == 0);
}

/* 12. Reset clears everything; new-slot discovery works even when armed. */
static void t12_reset_and_new_slot(void)
{
    watch_pathwatch_t pw; char conf[PW_IPLEN];
    SCENARIO("12 reset + armed new-slot discovery");
    setup(&pw, conf, "192.0.2.1");
    pw_observe(&pw, conf, "192.0.2.9", 1, NULL);

    pw_reset(&pw);
    CHECK(pw.n_known == 0);
    CHECK(pw.pending_ip[0] == '\0');
    CHECK(pw.pending_cycles == 0);
    CHECK(pw.ecmp_logged == 0);

    /* TTL expansion: empty confirmed slot is discovery even when armed */
    conf[0] = '\0';
    CHECK(pw_observe(&pw, conf, "198.51.100.7", 1, NULL) == PW_DISCOVERY);
    CHECK(strcmp(conf, "198.51.100.7") == 0);
    CHECK(in_set(&pw, "198.51.100.7"));
}

int main(void)
{
    printf("pathwatch unit tests:\n");
    t01_learning();
    t02_arming();
    t03_stable();
    t04_flap();
    t05_reroute();
    t06_route_restore();
    t07_ecmp_alternation();
    t08_alt_hosts();
    t09_cloaked();
    t10_churn();
    t11_overflow();
    t12_reset_and_new_slot();
    printf("%d checks, %d failures — %s\n",
           checks, failures, failures ? "FAIL" : "PASS");
    return failures ? 1 : 0;
}
