// Provisioning Portal pure helpers: HTML escaping and body-size bounds.
//
// SSIDs are untrusted external input, so the Portal must escape them for both
// attribute and text contexts and must refuse an oversized body before
// allocating. These checks are host-only; no network or device is involved.
#include "portal_html.h"
#include <cassert>
#include <cstdio>
#include <string>
#include <vector>

using namespace wearable;

// Reproduce exactly how the Portal composes one scan-result entry, so the
// assertions exercise the real template rather than a paraphrase.
static std::string render_entry(const std::string &ssid, int rssi) {
    const std::string attr = portal_attr_escape(ssid);
    const std::string text = portal_html_escape(ssid);
    return "<li><button data-ssid=\"" + attr + "\" type=\"button\">" + text +
           " &nbsp; " + std::to_string(rssi) + " dBm</button></li>";
}

// A hostile SSID must not introduce raw markup. Rather than re-implement an HTML
// parser, assert the property that matters for this template: the escaped SSID
// regions contain no raw '<', '>', '"' or "'", and the composed entry has no
// inline script or event-handler text.
static bool contains_raw_markup_char(const std::string &value) {
    return value.find('<') != std::string::npos || value.find('>') != std::string::npos ||
           value.find('"') != std::string::npos || value.find('\'') != std::string::npos;
}

// Count the attribute delimiters that must be exactly two for a well-formed
// single data-ssid="..." attribute.
static int count_data_ssid_quotes(const std::string &html) {
    const std::string needle = "data-ssid=\"";
    int count = 0;
    size_t pos = 0;
    while ((pos = html.find(needle, pos)) != std::string::npos) { ++count; pos += needle.size(); }
    return count;
}

int main() {
    // ---- Escaping covers every required character -------------------------
    assert(portal_html_escape("a&b") == "a&amp;b");
    assert(portal_html_escape("a<b") == "a&lt;b");
    assert(portal_html_escape("a>b") == "a&gt;b");
    assert(portal_html_escape("a\"b") == "a&quot;b");
    assert(portal_html_escape("a'b") == "a&#39;b");
    assert(portal_html_escape("Home & Office") == "Home &amp; Office");
    assert(portal_html_escape("plain") == "plain");
    // Characters are replaced, not deleted, so the original meaning survives.
    assert(portal_html_escape("A\"B").size() > std::string("A\"B").size());
    // The attribute escaper never leaves a raw quote or angle bracket.
    const std::string hostile = "\"><script>alert(1)</script>";
    const std::string escaped_attr = portal_attr_escape(hostile);
    assert(escaped_attr.find('"') == std::string::npos);
    assert(escaped_attr.find('<') == std::string::npos);
    assert(escaped_attr.find('>') == std::string::npos);

    // ---- Malicious / special SSIDs do not break the DOM -------------------
    const std::vector<std::string> cases = {
        "Home & Office",
        "A\"B",
        "<tag>",
        "\"><script>alert(1)</script>",
        "'''",
        "<<>>",
        "&amp;",
        "</button><script>x</script>",
        "normal-network",
        "中文网络",
    };
    for (const auto &ssid : cases) {
        const std::string html = render_entry(ssid, -42);
        // Exactly one attribute was opened, so the hostile SSID did not inject
        // a second data-ssid or close the button early.
        assert(count_data_ssid_quotes(html) == 1);
        assert(html.find("</button></li>") != std::string::npos);
        // The escaped SSID regions carry no raw markup characters, so the entry
        // structure cannot be broken or extended by the SSID.
        assert(!contains_raw_markup_char(portal_attr_escape(ssid)));
        assert(!contains_raw_markup_char(portal_html_escape(ssid)));
        assert(html.find("onerror=") == std::string::npos);
        assert(html.find("onload=") == std::string::npos);
        // A literal "<script" must only ever appear escaped.
        assert(html.find("<script") == std::string::npos);
        // The SSID text is still present (escaped), so the network remains
        // identifiable and selectable.
        if (ssid.rfind("normal", 0) == 0)
            assert(html.find("normal-network") != std::string::npos);
    }
    // The specific escalation attempt is fully neutralised.
    {
        const std::string html = render_entry("\"><script>alert(1)</script>", -40);
        assert(html.find("<script>") == std::string::npos);
        assert(html.find("alert(1)") != std::string::npos);  // text preserved, inert
        assert(html.find("&lt;script&gt;") != std::string::npos);
    }

    // ---- Body bound -------------------------------------------------------
    assert(portal_body_within_limit(0));
    assert(portal_body_within_limit(24));
    assert(portal_body_within_limit(PORTAL_MAX_BODY_BYTES));
    assert(!portal_body_within_limit(PORTAL_MAX_BODY_BYTES + 1));
    assert(!portal_body_within_limit(4096));
    assert(!portal_body_within_limit(size_t(-1)));
    // The bound comfortably fits a worst-case form body.
    const std::string worst_case_ssid(32, 'a');
    const std::string worst_case_password(64, 'b');
    const std::string worst_case_body = "ssid=" + worst_case_ssid + "&password=" + worst_case_password;
    assert(portal_body_within_limit(worst_case_body.size()));

    // ---- Form parsing still works with the pure helpers -------------------
    assert(portal_form_value("ssid=Home-WiFi&password=s3cret", "ssid") == "Home-WiFi");
    assert(portal_form_value("ssid=Home-WiFi&password=s3cret", "password") == "s3cret");
    assert(portal_form_value("ssid=My+Home&password=a%20b", "ssid") == "My Home");
    assert(portal_form_value("ssid=My+Home&password=a%20b", "password") == "a b");
    assert(portal_form_value("password=x", "ssid").empty());
    // A password containing '&' or '=' is decoded correctly because it is
    // percent-encoded by the client.
    const std::string encoded_body = "ssid=Net&password=a%26b%3Dc";
    assert(portal_form_value(encoded_body, "password") == "a&b=c");

    std::puts("Portal HTML escaping, hostile-SSID safety, body bound and form parse PASS");
}