import json
import subprocess
from pathlib import Path


CONTROLLER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-composer-viewport.js"


def run_controller_scenario(scenario: str, *, focus_within: bool = False) -> dict:
    harness = f"""
const createController = require({json.dumps(str(CONTROLLER))});
class Target {{
  constructor() {{ this.listeners = {{}}; }}
  addEventListener(name, handler) {{ (this.listeners[name] ||= new Set()).add(handler); }}
  removeEventListener(name, handler) {{ this.listeners[name]?.delete(handler); }}
  dispatch(name, event = {{}}) {{ for (const handler of this.listeners[name] || []) handler(event); }}
  count(name) {{ return this.listeners[name]?.size || 0; }}
}}
function element() {{
  const target = new Target();
  target.style = {{
    values: {{}},
    setProperty(name, value) {{ this.values[name] = value; }},
    removeProperty(name) {{ delete this.values[name]; }},
  }};
  target.scrollTop = 0;
  target.reveals = [];
  target.scrollIntoView = options => target.reveals.push(options);
  target.contains = candidate => candidate === target || candidate?.workspace === target;
  return target;
}}
const viewport = new Target();
viewport.offsetTop = 12;
viewport.height = 360;
const mediaQuery = {{matches:true}};
const panel = element();
const workspace = element();
const composer = element(); composer.workspace = workspace;
const submit = element(); submit.workspace = workspace;
const status = element(); status.workspace = workspace;
const document = {{activeElement:null}};
const scheduled = [];
const controller = createController({{
  entries:[{{panel, workspace, composer, submit, status, focusWithin:{json.dumps(focus_within)}}}], viewport, mediaQuery, document,
  schedule: callback => scheduled.push(callback),
}});
function flush() {{ while (scheduled.length) scheduled.shift()(); }}
{scenario}
"""
    result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
    return json.loads(result.stdout)


def test_mobile_composer_uses_visual_viewport_and_reveals_delivery_controls():
    result = run_controller_scenario(
        """
controller.start();
panel.scrollTop = 240;
document.activeElement = composer;
composer.dispatch('focus');
viewport.offsetTop = 20;
viewport.height = 300;
viewport.dispatch('resize');
flush();
process.stdout.write(JSON.stringify({
  geometry: panel.style.values,
  resizeListeners: viewport.count('resize'),
  scrollListeners: viewport.count('scroll'),
  revealCount: workspace.reveals.length,
  revealOptions: workspace.reveals.at(-1),
}));
"""
    )

    assert result == {
        "geometry": {
            "--composer-viewport-top": "20px",
            "--composer-viewport-height": "300px",
        },
        "resizeListeners": 1,
        "scrollListeners": 1,
        "revealCount": 2,
        "revealOptions": {"block": "end", "inline": "nearest"},
    }


def test_blur_restores_scroll_and_close_clears_geometry_without_duplicate_listeners():
    result = run_controller_scenario(
        """
controller.start();
controller.start();
panel.scrollTop = 240;
document.activeElement = composer;
composer.dispatch('focus');
composer.dispatch('focus');
document.activeElement = null;
workspace.dispatch('focusout');
flush();
const afterBlur = {scrollTop:panel.scrollTop, geometry:{...panel.style.values}, resize:viewport.count('resize')};
panel.scrollTop = 99;
document.activeElement = composer;
composer.dispatch('focus');
controller.close(panel);
flush();
process.stdout.write(JSON.stringify({
  afterBlur,
  afterClose:{scrollTop:panel.scrollTop, geometry:panel.style.values, resize:viewport.count('resize'), scroll:viewport.count('scroll')},
}));
"""
    )

    assert result == {
        "afterBlur": {"scrollTop": 240, "geometry": {}, "resize": 0},
        "afterClose": {"scrollTop": 99, "geometry": {}, "resize": 0, "scroll": 0},
    }


def test_desktop_and_missing_visual_viewport_are_noop():
    result = run_controller_scenario(
        """
mediaQuery.matches = false;
controller.start();
document.activeElement = composer;
composer.dispatch('focus');
process.stdout.write(JSON.stringify({geometry:panel.style.values, resize:viewport.count('resize'), reveals:workspace.reveals.length}));
"""
    )

    assert result == {"geometry": {}, "resize": 0, "reveals": 0}


def test_focus_within_reveals_each_active_capture_field_without_duplicate_viewport_listeners():
    result = run_controller_scenario(
        """
controller.start();
panel.scrollTop = 180;
document.activeElement = composer;
workspace.dispatch('focusin', {target:composer});
document.activeElement = submit;
workspace.dispatch('focusin', {target:submit});
viewport.dispatch('resize');
flush();
process.stdout.write(JSON.stringify({
  composerReveals:composer.reveals.length,
  submitReveals:submit.reveals.length,
  workspaceReveals:workspace.reveals.length,
  resizeListeners:viewport.count('resize'),
  scrollListeners:viewport.count('scroll'),
  geometry:panel.style.values,
}));
""",
        focus_within=True,
    )

    assert result == {
        "composerReveals": 1,
        "submitReveals": 2,
        "workspaceReveals": 0,
        "resizeListeners": 1,
        "scrollListeners": 1,
        "geometry": {
            "--composer-viewport-top": "12px",
            "--composer-viewport-height": "360px",
        },
    }
