"""Normalizer tests for Stable Assistant Turn Anchors (#3926)."""
from __future__ import annotations

import json
import shutil
import subprocess
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent
ANCHORS_JS = REPO / "static" / "assistant_turn_anchors.js"
MESSAGES_JS = REPO / "static" / "messages.js"
UI_JS = REPO / "static" / "ui.js"
SESSIONS_JS = REPO / "static" / "sessions.js"
NODE = shutil.which("node")


def _read(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def _normalizer_snapshot() -> dict:
    assert NODE, "node is required for assistant_turn_anchors.js normalizer tests"
    script = f"""
const fs = require('fs');
const vm = require('vm');
const src = fs.readFileSync({json.dumps(str(ANCHORS_JS))}, 'utf8');
const sandbox = {{window:{{}}}};
vm.createContext(sandbox);
vm.runInContext(src, sandbox, {{filename:'assistant_turn_anchors.js'}});
const api = sandbox.window.HermesAssistantTurnAnchors;
const context = {{
  session_id:'sid-1',
  turn_id:'turn-1',
  run_id:'run-1',
  stream_id:'stream-1',
}};
const liveToken = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'token',
  data:JSON.stringify({{text:'hello', session_id:'sid-1'}}),
  lastEventId:'run-1:7',
}}, context);
const replayToken = api.normalizeAssistantTurnAnchorSourceEvent({{
  event:'token',
  payload:{{text:'hello', session_id:'sid-1'}},
  event_id:'run-1:7',
  seq:7,
}}, context);
const settled = api.normalizeAssistantTurnAnchorSourceEvent({{
  source_type:'settled_message',
  payload:{{
    role:'assistant',
    content:'final',
    reasoning:'trace',
    _turnUsage:{{input_tokens:3, output_tokens:5}},
  }},
}}, context);
const artifact = api.normalizeAssistantTurnAnchorSourceEvent({{
  source_type:'artifact_reference',
  payload:{{path:'result.txt', kind:'workspace_file'}},
  event_id:'run-1:8',
}}, context);
const sideEffect = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'state_saved',
  payload:{{kind:'memory', name:'saved-state'}},
  event_id:'run-1:9',
}}, context);
const transport = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'stream_end',
  data:'{{"session_id":"sid-1"}}',
  event_id:'run-1:10',
}}, context);
const unknown = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'unknown_future_event',
  payload:{{text:'ignored'}},
}}, context);
const deduped = api.normalizeAssistantTurnAnchorSourceEvents([
  {{type:'token', data:'{{"text":"live"}}', lastEventId:'run-1:11'}},
  {{event:'token', payload:{{text:'replay'}}, event_id:'run-1:11', seq:11}},
  {{event:'reasoning', payload:{{text:'thinking'}}, event_id:'run-1:12', seq:12}},
], context);
const invalidPayload = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'token',
  data:'plain text chunk',
  event_id:'run-1:13',
}}, context);
const directParsedPayload = api.normalizeAssistantTurnAnchorSourceEvent({{
  source_type:'token',
  text:'direct parsed payload',
  event_id:'run-1:14',
  seq:14,
}}, context);
const eventTypePayload = api.normalizeAssistantTurnAnchorSourceEvent({{
  event_type:'tool',
  event_id:'run-1:15',
  tool_call_id:'tool-1',
  someField:'x',
}}, context);
const nonPlainPayload = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'token',
  payload:{{text:'date payload', meta:new Date('2026-06-11T00:00:00Z')}},
  event_id:'run-1:16',
}}, context);
const promotedIdentityPayload = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'token',
  payload:{{
    text:'identity fields should be promoted only',
    session_id:'payload-session',
    turn_id:'payload-turn',
    run_id:'payload-run',
    stream_id:'payload-stream',
    event_id:'payload-event',
    seq:99,
  }},
  event_id:'run-1:17',
}}, context);
const pollutionPayload = JSON.parse('{{"text":"clean","__proto__":{{"session_id":"polluted-session","run_id":"polluted-run"}},"constructor":{{"bad":true}},"prototype":{{"bad":true}}}}');
const pollutionAttempt = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'token',
  payload:pollutionPayload,
  event_id:'run-1:18',
}}, context);
const inheritedEvent = Object.create({{
  event_id:'evil:1',
  session_id:'evil-session',
  run_id:'evil-run',
  stream_id:'evil-stream',
  seq:666,
  created_at:'evil-time',
}});
inheritedEvent.type = 'token';
inheritedEvent.payload = {{text:'own event only'}};
const inheritedEnvelopeIdentity = api.normalizeAssistantTurnAnchorSourceEvent(inheritedEvent, context);
const inheritedContext = Object.create({{
  session_id:'evil-context-session',
  turn_id:'evil-context-turn',
  run_id:'evil-context-run',
  stream_id:'evil-context-stream',
  seq:777,
  created_at:'evil-context-time',
}});
const inheritedContextIdentity = api.normalizeAssistantTurnAnchorSourceEvent({{
  type:'token',
  payload:{{text:'own context only'}},
}}, inheritedContext);
const runSeqCollisionA = api.assistantTurnAnchorEventDedupeKey({{run_id:'a:b', seq:'c'}});
const runSeqCollisionB = api.assistantTurnAnchorEventDedupeKey({{run_id:'a', seq:'b:c'}});
const localCollisionA = api.assistantTurnAnchorEventDedupeKey({{session_id:'a:b', source_event_type:'tool', local_id:'c', seq:'d'}});
const localCollisionB = api.assistantTurnAnchorEventDedupeKey({{session_id:'a', source_event_type:'b:tool', local_id:'c', seq:'d'}});
const localNoSeqKey = api.assistantTurnAnchorEventDedupeKey({{session_id:'a', source_event_type:'token', local_id:'b'}});
console.log(JSON.stringify({{
  version: api.version,
  liveToken,
  replayToken,
  settled,
  artifact,
  sideEffect,
  transport,
  unknown,
  deduped,
  invalidPayload,
  directParsedPayload,
  eventTypePayload,
  nonPlainPayload,
  promotedIdentityPayload,
  pollutionAttempt,
  inheritedEnvelopeIdentity,
  inheritedContextIdentity,
  runSeqCollisionA,
  runSeqCollisionB,
  localCollisionA,
  localCollisionB,
  localNoSeqKey,
}}));
"""
    result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False)
    assert result.returncode == 0, result.stderr
    return json.loads(result.stdout)


def test_normalizer_maps_live_and_replay_to_same_anchor_event_identity():
    data = _normalizer_snapshot()
    live = data["liveToken"]
    replay = data["replayToken"]

    assert data["version"] == "slice8-renderer-snapshot-adapter"
    assert live["classification"] == "activity"
    assert live["dedupe_key"] == 'event_id:"run-1:7"'
    assert replay["dedupe_key"] == live["dedupe_key"]
    assert live["anchor_event"]["kind"] == "process_prose"
    assert live["anchor_event"]["source_event_type"] == "token"
    assert live["anchor_event"]["session_id"] == "sid-1"
    assert live["anchor_event"]["turn_id"] == "turn-1"
    assert live["anchor_event"]["run_id"] == "run-1"
    assert live["anchor_event"]["stream_id"] == "stream-1"
    assert live["anchor_event"]["seq"] == 7
    assert live["anchor_event"]["payload"] == {"text": "hello"}


def test_normalizer_keeps_artifact_side_effect_metadata_and_transport_distinct():
    data = _normalizer_snapshot()

    settled = data["settled"]
    assert settled["classification"] == "metadata"
    assert settled["anchor_event"]["kind"] is None
    assert settled["anchor_event"]["source_event_type"] == "settled_message"
    assert settled["anchor_event"]["payload"]["role"] == "assistant"
    assert settled["anchor_event"]["payload"]["_turnUsage"] == {"input_tokens": 3, "output_tokens": 5}

    artifact = data["artifact"]
    assert artifact["classification"] == "artifact"
    assert artifact["anchor_event"]["kind"] == "artifact_reference"
    assert artifact["anchor_event"]["payload"] == {"kind": "workspace_file", "path": "result.txt"}

    side_effect = data["sideEffect"]
    assert side_effect["classification"] == "side_effect"
    assert side_effect["anchor_event"]["kind"] is None
    assert side_effect["anchor_event"]["payload"] == {"kind": "memory", "name": "saved-state"}

    transport = data["transport"]
    assert transport["classification"] == "transport"
    assert transport["anchor_event"]["kind"] is None
    assert transport["anchor_event"]["status"] == "transport_closed"


def test_normalizer_excludes_unknown_sources_and_sanitizes_non_json_data():
    data = _normalizer_snapshot()

    assert data["unknown"] == {
        "classification": "excluded",
        "source_event_type": "unknown_future_event",
        "anchor_event": None,
        "dedupe_key": "",
    }
    assert data["invalidPayload"]["anchor_event"]["payload"] == {"text": "plain text chunk"}
    assert data["invalidPayload"]["dedupe_key"] == 'event_id:"run-1:13"'
    assert data["directParsedPayload"]["anchor_event"]["payload"] == {"text": "direct parsed payload"}
    assert data["directParsedPayload"]["anchor_event"]["seq"] == 14


def test_normalizer_strips_discriminators_and_promoted_identity_fields_from_payload():
    data = _normalizer_snapshot()

    event_type_payload = data["eventTypePayload"]
    assert event_type_payload["anchor_event"]["source_event_type"] == "tool"
    assert event_type_payload["anchor_event"]["payload"] == {
        "someField": "x",
        "tool_call_id": "tool-1",
    }

    promoted = data["promotedIdentityPayload"]
    assert promoted["anchor_event"]["event_id"] == "run-1:17"
    assert promoted["anchor_event"]["session_id"] == "payload-session"
    assert promoted["anchor_event"]["turn_id"] == "payload-turn"
    assert promoted["anchor_event"]["run_id"] == "payload-run"
    assert promoted["anchor_event"]["stream_id"] == "payload-stream"
    assert promoted["anchor_event"]["seq"] == 99
    assert promoted["anchor_event"]["payload"] == {
        "text": "identity fields should be promoted only"
    }


def test_normalizer_rejects_pollution_keys_and_inherited_identity_fields():
    data = _normalizer_snapshot()

    pollution = data["pollutionAttempt"]
    assert pollution["dedupe_key"] == 'event_id:"run-1:18"'
    assert pollution["anchor_event"]["session_id"] == "sid-1"
    assert pollution["anchor_event"]["run_id"] == "run-1"
    assert pollution["anchor_event"]["payload"] == {"text": "clean"}

    inherited_event = data["inheritedEnvelopeIdentity"]["anchor_event"]
    assert inherited_event["event_id"] is None
    assert inherited_event["session_id"] == "sid-1"
    assert inherited_event["run_id"] == "run-1"
    assert inherited_event["stream_id"] == "stream-1"
    assert inherited_event["seq"] is None
    assert inherited_event["created_at"] is None
    assert inherited_event["payload"] == {"text": "own event only"}

    inherited_context = data["inheritedContextIdentity"]["anchor_event"]
    assert inherited_context["event_id"] is None
    assert inherited_context["session_id"] is None
    assert inherited_context["turn_id"] is None
    assert inherited_context["run_id"] is None
    assert inherited_context["stream_id"] is None
    assert inherited_context["seq"] is None
    assert inherited_context["created_at"] is None
    assert inherited_context["payload"] == {"text": "own context only"}


def test_normalizer_marks_non_plain_payload_objects_explicitly():
    data = _normalizer_snapshot()

    assert data["nonPlainPayload"]["anchor_event"]["payload"] == {
        "meta": "[Object]",
        "text": "date payload",
    }


def test_batch_normalizer_dedupes_live_plus_replay_by_event_envelope():
    data = _normalizer_snapshot()
    deduped = data["deduped"]

    assert [item["dedupe_key"] for item in deduped] == [
        'event_id:"run-1:11"',
        'event_id:"run-1:12"',
    ]
    assert [item["anchor_event"]["kind"] for item in deduped] == [
        "process_prose",
        "reasoning",
    ]
    assert deduped[0]["anchor_event"]["payload"] == {"text": "live"}


def test_structured_fallback_dedupe_keys_do_not_collide_on_delimiters():
    data = _normalizer_snapshot()

    assert data["runSeqCollisionA"] == 'run_seq:["a:b","c"]'
    assert data["runSeqCollisionB"] == 'run_seq:["a","b:c"]'
    assert data["runSeqCollisionA"] != data["runSeqCollisionB"]
    assert data["localCollisionA"] == 'local:["a:b","tool","c","d"]'
    assert data["localCollisionB"] == 'local:["a","b:tool","c","d"]'
    assert data["localCollisionA"] != data["localCollisionB"]
    assert data["localNoSeqKey"] == ""


def test_normalizer_helpers_remain_unwired_from_rendering_and_live_hot_paths():
    helper_names = [
        "normalizeAssistantTurnAnchorSourceEvent",
        "normalizeAssistantTurnAnchorSourceEvents",
        "applyAssistantTurnAnchorSourceEvents",
    ]
    for helper in helper_names:
        assert helper not in _read(UI_JS)
        assert helper not in _read(SESSIONS_JS)
        assert helper not in _read(MESSAGES_JS)

    messages_src = _read(MESSAGES_JS)
    assert "createAssistantTurnAnchorRegistry" in messages_src
    assert "applyAssistantTurnAnchorSourceEvent" in messages_src
