"""US-035: tracker web dashboard — served from any tracker, embedded asset."""

import http.client
import json
import time
import urllib.request

from meshnet_contracts import LocalSolanaContracts
from meshnet_tracker.accounts import AccountStore
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer

PANELS = [
    "Tracker hive", "Nodes &amp; coverage", "Client balances",
    "Node pending payouts", "Settlement history",
    "Strikes / bans / forfeitures", "Model usage", "Call wall",
    "Usage summary", "Node throughput", "Request history",
    "Chat / inference",
    "Console output",
    "Test run status", "Tests &amp; suites", "Test output",
]


def test_dashboard_served_with_all_panels():
    "Dashboard served with all panels\n\nTags: dashboard, http"
    tracker = TrackerServer(billing=BillingLedger())
    port = tracker.start()
    try:
        html = urllib.request.urlopen(
            f"http://127.0.0.1:{port}/dashboard"
        ).read().decode()
        for panel in PANELS:
            assert panel in html
        assert '<link rel="icon" type="image/svg+xml" href="/favicon.svg">' in html
        favicon = urllib.request.urlopen(f"http://127.0.0.1:{port}/favicon.svg").read()
        assert favicon.startswith(b"<svg")
        assert b"meshnet" in favicon
        assert "<script>" in html  # polling client embedded, no build step
        assert "resolveModelGroup" in html
        assert "buildModelAliasMap" in html
        assert "modelAliasKey(raw)" in html
        assert "@media (min-width:900px)" in html
        assert "grid-template-columns:repeat(4,minmax(0,1fr));" in html
        assert ".wide { grid-column:span 2; }" in html
        assert 'onclick="clearConsole()"' in html
        assert "let consoleClearedAt = 0;" in html
        assert "max-height:520px; overflow-y:auto; overflow-x:auto;" in html
        assert "const CONSOLE_MAX_LINES = 1000;" in html
        assert "events.slice(-CONSOLE_MAX_LINES)" in html
    finally:
        tracker.stop()


def test_tracker_root_redirects_to_dashboard():
    "Tracker root redirects to dashboard\n\nTags: dashboard, http"
    tracker = TrackerServer()
    port = tracker.start()
    try:
        connection = http.client.HTTPConnection("127.0.0.1", port)
        connection.request("GET", "/")
        response = connection.getresponse()
        location = response.getheader("Location")
        response.read()
        connection.close()
    finally:
        tracker.stop()

    assert response.status == 302
    assert location == "/dashboard"


def test_dashboard_chat_uses_streaming_fetch():
    "Dashboard chat uses streaming fetch\n\nTags: dashboard, http, streaming"
    tracker = TrackerServer(billing=BillingLedger())
    port = tracker.start()
    try:
        html = urllib.request.urlopen(
            f"http://127.0.0.1:{port}/dashboard"
        ).read().decode()
    finally:
        tracker.stop()

    assert "stream: true" in html
    assert ".body.getReader()" in html
    assert '=== "[DONE]"' in html
    assert 'console.error("chat stream failed", err)' in html
    assert "preloadChatModels" in html
    assert "renderChatModels(true)" in html


def test_dashboard_allows_admin_to_request_selected_model_load():
    "Dashboard allows admin to request selected model load\n\nTags: dashboard, http"
    tracker = TrackerServer()
    port = tracker.start()
    try:
        html = urllib.request.urlopen(
            f"http://127.0.0.1:{port}/dashboard"
        ).read().decode()
    finally:
        tracker.stop()

    assert 'id="request-model-load"' in html
    assert "requestSelectedModelLoad" in html
    assert '"/v1/models/load"' in html
    assert '$("request-model-load").style.display = enabled ? "" : "none"' in html


def test_dashboard_exposes_admin_model_inventory_and_release_controls():
    "Admin placement controls show the full model inventory and can release capacity."
    html = _dashboard_html()

    assert 'id="admin-model-placement"' in html
    assert "renderAdminModelPlacement" in html
    assert '"/v1/models/release"' in html
    assert "requestAdminModelLoad" in html
    assert "releaseAdminModel" in html
    assert 'data-admin-model-load=' in html
    assert 'data-admin-model-release=' in html
    assert "admin-model-placement-status" in html
    assert 'id="admin-node-pool"' in html
    assert "renderAdminNodePool" in html
    assert "model drive" in html
    # RAM and VRAM live in the network-map capacity object, not at node top level.
    assert "node.ram_bytes = cap.ram_bytes" in html
    assert "node.vram_bytes = cap.vram_bytes" in html
    assert 'id="model-placement-dialog"' in html
    assert "chooseModelPlacementNode" in html
    assert "node_id: nodeId" in html
    assert "modelAliasKey(node.model)" in html
    assert 'id="model-placement-replace"' in html
    assert 'id="model-placement-confirm"' in html
    assert 'id="model-placement-replace-error"' in html
    assert "force: replacing" in html
    assert "Tick the box to confirm" in html
    assert "releaseAllNodeModels" in html
    assert '"/v1/nodes/release-all"' in html
    assert "model RAM" in html
    assert "loaded_model_bytes" in html


def test_network_map_includes_node_friendly_name():
    "Network map includes node friendly name\n\nTags: dashboard, http"
    tracker = TrackerServer()
    port = tracker.start()
    try:
        body = json.dumps({
            "endpoint": "http://127.0.0.1:9010",
            "model": "stub-model",
            "shard_start": 0,
            "shard_end": 3,
            "hardware_profile": {},
            "friendly_name": "Kitchen GPU",
        }).encode()
        req = urllib.request.Request(
            f"http://127.0.0.1:{port}/v1/nodes/register",
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        urllib.request.urlopen(req).read()
        network = json.loads(
            urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/network/map").read()
        )
    finally:
        tracker.stop()

    assert network["nodes"][0]["friendly_name"] == "Kitchen GPU"


def test_dashboard_chat_model_selector_shows_health_and_speed():
    "Dashboard chat model selector shows health and speed\n\nTags: dashboard, http"
    tracker = TrackerServer()
    port = tracker.start()
    try:
        html = urllib.request.urlopen(
            f"http://127.0.0.1:{port}/dashboard"
        ).read().decode()
    finally:
        tracker.stop()

    assert "chatModelHealthHp" in html
    assert "modelServedCopiesFromMap" in html
    assert "nodeDisplayName" in html
    assert "accountDisplayName" in html
    assert "saveNickname" in html
    assert "chatModelTypicalTps" in html
    assert "chatStreamStatsText" in html
    assert "chat-stream-stats" in html
    assert "chatMessageRowHtml" in html
    assert "chatModelOptionLabel" in html
    assert "findRoutingForModel" in html
    assert "tok/s" in html
    assert "toFixed(2)}HP" in html or '${copies(v)}HP' in html


def test_dashboard_chat_sessions_use_delegated_handlers():
    "Dashboard chat sessions use delegated handlers\n\nTags: dashboard, http"
    tracker = TrackerServer()
    port = tracker.start()
    try:
        html = urllib.request.urlopen(
            f"http://127.0.0.1:{port}/dashboard"
        ).read().decode()
    finally:
        tracker.stop()

    assert "bindChatSessionList" in html
    assert "dataset.sessionId" in html
    assert "dataset.deleteSession" in html
    assert '[data-session-id]' in html
    assert 'onclick="selectChatSession(' not in html
    assert 'onclick="deleteChatSession(' not in html


def test_dashboard_incremental_refresh_helpers():
    "Dashboard incremental refresh helpers\n\nTags: dashboard, http"
    tracker = TrackerServer()
    port = tracker.start()
    try:
        html = urllib.request.urlopen(
            f"http://127.0.0.1:{port}/dashboard"
        ).read().decode()
    finally:
        tracker.stop()

    assert "renderIfChanged" in html
    assert "syncKeyedList" in html
    assert "refreshBlocked" in html
    assert "patchAccountPanelView" in html
    assert "buildAccountPanelShell" in html
    assert "refreshActiveTab" in html
    assert "TAB_FETCHERS" in html
    assert "loadAccountSummary" in html
    assert "loadAccountUsage" in html
    assert "fetchOverviewTab" in html
    assert "pollCallWallIfIdle" in html
    assert "pendingChatModelRefresh" in html
    assert 'renderChatHistory(true)' in html
    assert "renderChatHistory();" not in html
    assert "refreshIfIdle" not in html
    assert "refreshAccountIfIdle" not in html
    assert "setInterval(refreshIfIdle" not in html


def test_dashboard_served_by_follower():
    "A tracker that is not the leader (unreachable peers → never elected)\n\nTags: dashboard, http"
    tracker = TrackerServer(
        billing=BillingLedger(),
        cluster_peers=["http://127.0.0.1:1", "http://127.0.0.1:2"],
    )
    port = tracker.start()
    try:
        response = urllib.request.urlopen(f"http://127.0.0.1:{port}/dashboard")
        assert response.status == 200
        assert "meshnet tracker" in response.read().decode()
    finally:
        tracker.stop()


def test_registry_wallets_endpoint():
    "Registry wallets endpoint\n\nTags: dashboard, http, security, wallet"
    contracts = LocalSolanaContracts()
    contracts.registry.submit_stake("wallet-a", 100)
    contracts.registry.record_strike("wallet-a")
    accounts = AccountStore()
    admin = accounts.register(email="admin@example.com", password="admin-pass-123")
    session = accounts.create_session(admin["account_id"])
    tracker = TrackerServer(contracts=contracts, accounts=accounts)
    port = tracker.start()
    try:
        req = urllib.request.Request(
            f"http://127.0.0.1:{port}/v1/registry/wallets",
            headers={"Authorization": f"Bearer {session}"},
        )
        data = json.loads(urllib.request.urlopen(req).read())
        assert data["wallets"]["wallet-a"]["strike_count"] == 1
        assert data["wallets"]["wallet-a"]["banned"] is False
    finally:
        tracker.stop()


def test_console_endpoint_exposes_tracker_events():
    "Console endpoint exposes tracker events\n\nTags: dashboard, http"
    tracker = TrackerServer()
    port = tracker.start()
    try:
        body = json.dumps({
            "endpoint": "http://127.0.0.1:9001",
            "model": "stub-model",
            "shard_start": 0,
            "shard_end": 3,
            "hardware_profile": {},
        }).encode()
        req = urllib.request.Request(
            f"http://127.0.0.1:{port}/v1/nodes/register",
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        urllib.request.urlopen(req).read()

        data = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
    finally:
        tracker.stop()

    assert any(event["message"] == "node registered" for event in data["events"])


def test_console_node_lifecycle_events_include_model_health():
    "Console node lifecycle events include model health\n\nTags: dashboard, http"
    tracker = TrackerServer(heartbeat_timeout=0.05)
    port = tracker.start()
    try:
        body = json.dumps({
            "endpoint": "http://127.0.0.1:9002",
            "model": "console-health-test",
            "hf_repo": "example/console-health-test",
            "num_layers": 4,
            "shard_start": 0,
            "shard_end": 1,
            "hardware_profile": {},
        }).encode()
        req = urllib.request.Request(
            f"http://127.0.0.1:{port}/v1/nodes/register",
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        urllib.request.urlopen(req).read()

        registered = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
        registered_event = next(
            event for event in registered["events"]
            if event["message"] == "node registered"
        )
        assert registered_event["fields"]["model_health"]["served_model_copies"] == 0.5
        assert registered_event["fields"]["model_health"]["coverage_percentage"] == 50.0

        time.sleep(0.06)
        urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/network/map").read()
        expired = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
        expired_event = next(
            event for event in expired["events"]
            if event["message"] == "node expired"
        )
        assert expired_event["fields"]["model_health"]["served_model_copies"] == 0.0
        assert expired_event["fields"]["model_health"]["coverage_percentage"] == 0.0
    finally:
        tracker.stop()


def _dashboard_html(**kwargs) -> str:
    tracker = TrackerServer(**kwargs)
    port = tracker.start()
    try:
        return urllib.request.urlopen(
            f"http://127.0.0.1:{port}/dashboard"
        ).read().decode()
    finally:
        tracker.stop()


def test_dashboard_testing_tab_is_admin_only():
    "US-002: the Testing tab ships hidden and is only revealed for admins.\n\nTags: dashboard, http"
    html = _dashboard_html()

    # Tab button exists but is hidden until setAdminMode(true) reveals it.
    assert '<button id="tab-testing" style="display:none"' in html
    assert "switchDashboardTab('testing')" in html
    assert '$("tab-testing").style.display = enabled ? "" : "none"' in html

    # Every Testing panel is data-admin-only, so updateSectionVisibility()
    # (adminOnly && !isAdmin -> hidden) keeps them hidden for non-admins.
    for panel in ("testing-status", "testing-targets", "testing-log"):
        assert f'id="{panel}"' in html
    assert html.count('data-tab="testing" data-admin-only') == 3

    # Selecting the tab without admin rights falls back to overview, and losing
    # admin rights while on it kicks the user out.
    assert 'if ((name === "admin" || name === "testing") && !isAdmin) name = "overview";' in html
    assert 'if (!enabled && (dashboardTab === "admin" || dashboardTab === "testing"))' in html


def test_dashboard_testing_tab_uses_dynamic_api_and_run_controls():
    "US-002: targets come from the API, never a hardcoded list.\n\nTags: dashboard, http"
    html = _dashboard_html()

    # Dynamic collection + run + status endpoints from US-001.
    assert 'apiCall(`/v1/tests${refresh ? "?refresh=1" : ""}`)' in html
    assert 'apiCall("/v1/tests/run", "POST", { target })' in html
    assert 'apiCall("/v1/tests/status")' in html
    assert "fetchTestingTab" in html
    assert "testing: fetchTestingTab" in html  # registered in TAB_FETCHERS

    # Targets are rendered from the API payload (tests + suites), not literals.
    assert "testCollection.suites" in html
    assert "testCollection.tests" in html
    assert "testCollection.test_metadata" in html
    assert "testCollection.tags" in html
    assert 'data-test-target="suite:all"' in html
    assert 'id="testing-tag-filter"' in html
    assert "Run ${esc(t.name)}" in html
    assert "renderTestTargets" in html

    # Run buttons are per-target and delegated; disabled while a run is active.
    assert 'data-test-target="${esc(t.id)}"' in html
    assert "const disabled = testRunActive() ? \" disabled\" : \"\";" in html
    assert "if (testRunActive()) return;" in html  # runTest() guards re-entry
    assert 'button = event.target.closest("[data-test-target]")' in html

    # API errors are surfaced rather than swallowed.
    assert "showTestingError" in html
    assert "testingErrorText" in html
    assert 'id="testing-error"' in html


def test_dashboard_testing_tab_renders_status_and_bounded_log():
    "US-002: state, timings, exit code, outcome and an auto-refreshing log.\n\nTags: dashboard, http"
    html = _dashboard_html()

    assert "renderTestRunStatus" in html
    assert "renderTestLog" in html
    for field in ("target", "state", "outcome", "started", "ended", "elapsed", "exit code"):
        assert f'"{field}"' in html
    assert "testRun.exit_code" in html
    assert "testRun.started_at" in html
    assert "testRun.finished_at" in html
    assert "testRun.elapsed_seconds" in html
    assert "fmtElapsed" in html

    # Success/failure is derived from the run status, not guessed from the log.
    assert '<span class="ok">success</span>' in html
    assert '<span class="bad">failure</span>' in html

    # Bounded log view + auto-refresh while the run is in flight.
    assert "TEST_LOG_MAX_LINES" in html
    assert "lines.slice(-TEST_LOG_MAX_LINES)" in html
    assert "pollTestRunIfActive" in html
    assert "setInterval(pollTestRunIfActive, TEST_RUN_POLL_MS)" in html
