import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


ROOT = Path(__file__).parents[1]
READINESS = ROOT / "frontend" / "today-readiness.js"
SERVICE_WORKER = ROOT / "frontend" / "service-worker.js"


def run_node(script):
    return subprocess.run(
        ["node", "-e", script], check=True, capture_output=True, text=True
    ).stdout


def test_readiness_opens_ready_target_without_inspecting_the_rest_of_the_queue():
    script = f"""
const createTodayReadiness = require({json.dumps(str(READINESS))});
const calls = [];
const inspections = [];
const item = (number, kind='issue') => ({{kind, repository:'stackchain/dashboard', number, title:'Item ' + number}});
const readiness = createTodayReadiness({{
  inspect: async candidate => {{ inspections.push(candidate.number); return {{available:true, dependencies:[]}}; }},
  onOpen: (action, candidate) => calls.push(['open', action, candidate.number]),
  onGate: state => calls.push(['gate', state.status]),
}});
readiness.run('start', [item(1), item(2), item(3)]).then(result =>
  process.stdout.write(JSON.stringify({{result, calls, inspections}}))
);
"""
    assert json.loads(run_node(script)) == {
        "result": "opened",
        "calls": [["open", "start", 1]],
        "inspections": [1],
    }


def test_readiness_checks_fallbacks_concurrently_but_preserves_queue_order():
    script = f"""
const createTodayReadiness = require({json.dumps(str(READINESS))});
const items = [1, 2, 3].map(number => ({{kind:'issue', repository:'r', number}}));
const starts = [];
const resolvers = {{}};
const readiness = createTodayReadiness({{
  inspect: candidate => {{
    starts.push(candidate.number);
    if (candidate.number === 1) return Promise.resolve({{available:true, dependencies:[{{state:'open'}}]}});
    return new Promise(resolve => {{ resolvers[candidate.number] = resolve; }});
  }},
  onOpen: () => undefined,
  onGate: state => {{ globalThis.gated = state.nextReady.number; }},
}});
(async () => {{
  const running = readiness.run('start', items);
  await new Promise(resolve => setImmediate(resolve));
  const startedTogether = starts.slice();
  if (resolvers[3]) resolvers[3]({{available:true, dependencies:[]}});
  resolvers[2]({{available:true, dependencies:[]}});
  await new Promise(resolve => setImmediate(resolve));
  if (!startedTogether.includes(3)) resolvers[3]({{available:true, dependencies:[]}});
  const result = await running;
  process.stdout.write(JSON.stringify({{result, startedTogether, gated:globalThis.gated}}));
}})();
"""
    assert json.loads(run_node(script)) == {
        "result": "gated",
        "startedTogether": [1, 2, 3],
        "gated": 2,
    }


def test_readiness_gates_blocked_target_and_offers_earliest_ready_without_reordering():
    script = f"""
const createTodayReadiness = require({json.dumps(str(READINESS))});
const calls = [];
const items = [1, 2, 3].map(number => ({{kind:'issue', repository:'r', number, title:'Issue ' + number}}));
const readiness = createTodayReadiness({{
  inspect: async candidate => candidate.number === 1
    ? {{available:true, dependencies:[{{repository:'r', number:99, title:'Prerequisite', state:'open', url:'https://forge.example/r/issues/99'}}]}}
    : {{available:true, dependencies:[]}},
  onOpen: (action, candidate) => calls.push(['open', action, candidate.number]),
  onGate: state => calls.push(['gate', state.status, state.target.number, state.nextReady.number, state.items.map(item => item.number)]),
}});
readiness.run('resume', items, items[0]).then(async result => {{
  const opened = readiness.startNextReady();
  process.stdout.write(JSON.stringify({{result, opened, calls, order:items.map(item => item.number)}}));
}});
"""
    assert json.loads(run_node(script)) == {
        "result": "gated",
        "opened": True,
        "calls": [
            ["gate", "blocked", 1, 2, [1, 2, 3]],
            ["open", "resume", 2],
        ],
        "order": [1, 2, 3],
    }


def test_readiness_previews_only_a_current_blocker_without_closing_or_reordering_gate():
    script = f"""
const createTodayReadiness = require({json.dumps(str(READINESS))});
const previews = [];
const items = [1, 2].map(number => ({{kind:'issue', repository:'r', number}}));
const blocker = {{repository:'r', number:99, title:'Prerequisite', state:'open'}};
const readiness = createTodayReadiness({{
  inspect: async candidate => candidate.number === 1
    ? {{available:true, dependencies:[blocker]}}
    : {{available:true, dependencies:[]}},
  onPreview: candidate => previews.push(candidate.number),
}});
(async () => {{
  await readiness.run('start', items, items[0]);
  const accepted = readiness.previewBlocker(blocker);
  const rejected = readiness.previewBlocker({{repository:'r', number:100}});
  const snapshot = readiness.snapshot();
  process.stdout.write(JSON.stringify({{
    accepted, rejected, previews,
    open:snapshot.status === 'blocked',
    target:snapshot.target.number,
    order:snapshot.items.map(item => item.number),
  }}));
}})();
"""
    assert json.loads(run_node(script)) == {
        "accepted": True,
        "rejected": False,
        "previews": [99],
        "open": True,
        "target": 1,
        "order": [1, 2],
    }


def test_readiness_never_calls_unknown_ready_and_supports_retry_or_explicit_override():
    script = f"""
const createTodayReadiness = require({json.dumps(str(READINESS))});
const calls = [];
const target = {{kind:'issue', repository:'r', number:1}};
let available = false;
const readiness = createTodayReadiness({{
  inspect: async () => available ? {{available:true, dependencies:[]}} : {{available:false, dependencies:[]}},
  onOpen: (action, candidate) => calls.push(['open', action, candidate.number]),
  onGate: state => calls.push(['gate', state.status]),
}});
readiness.run('next', [target], target).then(async result => {{
  available = true;
  const retried = await readiness.retry();
  available = false;
  await readiness.run('continue', [target], target);
  const overridden = readiness.workAnyway();
  process.stdout.write(JSON.stringify({{result, retried, overridden, calls}}));
}});
"""
    assert json.loads(run_node(script)) == {
        "result": "gated",
        "retried": "opened",
        "overridden": True,
        "calls": [
            ["gate", "unknown"],
            ["open", "next", 1],
            ["gate", "unknown"],
            ["open", "continue", 1],
        ],
    }


def test_readiness_treats_non_issue_work_as_runnable_without_dependency_io():
    script = f"""
const createTodayReadiness = require({json.dumps(str(READINESS))});
let inspections = 0;
const pull = {{kind:'pull', repository:'r', number:7}};
const readiness = createTodayReadiness({{
  inspect: async () => {{ inspections += 1; throw new Error('not expected'); }},
  onOpen: () => undefined,
  onGate: () => undefined,
}});
readiness.run('start', [pull]).then(result =>
  process.stdout.write(JSON.stringify({{result, inspections}}))
);
"""
    assert json.loads(run_node(script)) == {"result": "opened", "inspections": 0}


@pytest.mark.anyio
async def test_dashboard_exposes_mobile_today_readiness_gate_and_runtime():
    html = await dashboard()

    assert '<script src="static/today-readiness.js"></script>' in html
    assert 'id="today-readiness-sheet"' in html
    assert 'id="today-readiness-next"' in html
    assert 'id="today-readiness-anyway"' in html
    assert 'id="today-readiness-retry"' in html
    assert 'id="today-readiness-plan"' in html
    assert "createTodayReadiness({" in html
    assert "runTodayTransition('start'" in html
    assert "runTodayTransition('resume'" in html
    assert "runTodayTransition('continue'" in html
    assert "runTodayTransition('next'" in html
    assert "runTodayTransition('complete'" in html


@pytest.mark.anyio
async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate():
    html = await dashboard()

    assert 'data-today-blocker-index="' in html
    assert "todayReadiness.previewBlocker(blocker)" in html
    assert "searchPreview.open({ ...blocker, kind:'issue' })" in html
    assert "taskOverlayHistory.open('search-preview')" in html
    assert "suspendTodayReadiness()" in html
    assert "todayReadiness.cancel()" in html
    assert "todayReadinessBlockerFocus" in html


def test_readiness_runtime_is_available_in_offline_shell():
    service_worker = SERVICE_WORKER.read_text()

    assert "const CACHE = 'stackchain-dashboard-shell-v127';" in service_worker
    assert "BASE + 'static/today-readiness.js'" in service_worker


def test_mobile_readiness_gate_has_touch_safe_wrapping_actions():
    css = (ROOT / "frontend" / "dashboard.css").read_text()

    assert ".today-readiness-actions button" in css
    assert "min-height:44px" in css
    assert ".today-readiness-item" in css
    assert "overflow-wrap:anywhere" in css
    assert "env(safe-area-inset-bottom)" in css
    assert ".today-readiness-blocker" in css
    assert "min-height:44px" in css
