import json
import subprocess
from pathlib import Path


ROOT = Path(__file__).parents[1]
MODULE = ROOT / "frontend" / "search-preview.js"


def run(script: str) -> dict:
    harness = f"""
require({json.dumps(str(MODULE))});
const createDetailWatch = globalThis.createDetailWatch;
const state = {{ requests:[], renders:[], refreshes:0 }};
const feature = createDetailWatch({{
  fetchJson: async (path, options={{}}) => {{
    state.requests.push({{path,method:options.method || 'GET'}});
    return {{watching:false,following_synced:true}};
  }},
  refreshFollowing: async () => {{ state.refreshes += 1; }},
  onState: (status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{ {script} }})().catch(error => {{ console.error(error); process.exit(1); }});
"""
    completed = subprocess.run(["node", "-e", harness], text=True, capture_output=True, check=True)
    return json.loads(completed.stdout)


def test_detail_watch_loads_authoritative_typed_subscription_state():
    result = run("""
await feature.open({repository:'stackchain/api',kind:'pull',number:84,state:'open'});
process.stdout.write(JSON.stringify(state));
""")

    assert result["requests"] == [{
        "path": "api/v1/repos/stackchain/api/issues/84/preview/subscription?kind=pull",
        "method": "GET",
    }]
    assert result["renders"] == [
        {"status": "loading", "watching": False},
        {"status": "ready", "watching": False},
    ]
    assert result["refreshes"] == 0


def test_detail_watch_mutation_is_single_flight_and_refreshes_following_after_confirmation():
    script = f"""
require({json.dumps(str(MODULE))});
const createDetailWatch = globalThis.createDetailWatch;
const state={{requests:[],renders:[],refreshes:0}};
let finishMutation;
const feature=createDetailWatch({{
  fetchJson:async (path,options={{}}) => {{
    state.requests.push({{path,method:options.method || 'GET'}});
    if (!options.method) return {{watching:false}};
    return new Promise(resolve => {{finishMutation=() => resolve({{watching:true,following_synced:true,following_count:3}});}});
  }},
  refreshFollowing:async () => {{state.refreshes += 1;}},
  onState:(status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{
  const item={{repository:'stackchain/api',kind:'issue',number:42,state:'open'}};
  await feature.open(item);
  const first=feature.toggle();
  const second=feature.toggle();
  await new Promise(resolve => setImmediate(resolve));
  state.samePromise=first === second;
  finishMutation();
  await Promise.all([first,second]);
  process.stdout.write(JSON.stringify(state));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
    result = json.loads(subprocess.run(
        ["node", "-e", script], text=True, capture_output=True, check=True
    ).stdout)

    assert result["samePromise"] is True
    assert [request["method"] for request in result["requests"]] == ["GET", "PUT"]
    assert result["refreshes"] == 1
    assert result["renders"][-2]["status"] == "watching"
    assert result["renders"][-1]["status"] == "watched"
    assert result["renders"][-1]["watching"] is True


def test_detail_watch_partial_sync_never_claims_success_or_refreshes_following():
    script = f"""
require({json.dumps(str(MODULE))});
const createDetailWatch = globalThis.createDetailWatch;
const state={{renders:[],refreshes:0}};
const feature=createDetailWatch({{
  fetchJson:async (_path,options={{}}) => options.method
    ? {{watching:true,following_synced:false,error:'Following could not sync.'}}
    : {{watching:false}},
  refreshFollowing:async () => {{state.refreshes += 1;}},
  onState:(status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{
  await feature.open({{repository:'stackchain/api',kind:'issue',number:42,state:'open'}});
  try {{ await feature.toggle(); }} catch (error) {{ state.rejected=error.message; }}
  process.stdout.write(JSON.stringify(state));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
    result = json.loads(subprocess.run(
        ["node", "-e", script], text=True, capture_output=True, check=True
    ).stdout)

    assert result["rejected"] == "Following could not sync."
    assert result["refreshes"] == 0
    assert result["renders"][-1]["status"] == "error"
    assert result["renders"][-1]["watching"] is False
    assert all(state["status"] != "watched" for state in result["renders"])


def test_detail_watch_is_wired_into_both_mobile_work_details_and_bundle():
    html = (ROOT / "frontend" / "index.html").read_text()
    dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
    css = (ROOT / "frontend" / "dashboard.css").read_text()
    bundle = (ROOT / "src" / "frontend_bundle.py").read_text()
    workflow = (ROOT / ".gitea" / "workflows" / "ci.yml").read_text()
    search_preview = (ROOT / "frontend" / "search-preview.js").read_text()

    assert '<button id="watch-issue-detail" type="button">Watch issue</button>' in html
    assert '<button id="watch-pull-detail" type="button">Watch pull request</button>' in html
    assert 'id="issue-watch-status"' in html
    assert 'id="pull-watch-status"' in html
    assert 'static/search-preview.js' in bundle
    assert 'root.createDetailWatch' in search_preview
    assert 'function bindDetailWatch(' in dashboard
    assert 'issueDetailWatch.open(item)' in dashboard
    assert 'pullDetailWatch.open(item)' in dashboard
    assert '#watch-issue-detail' in css
    assert '#watch-pull-detail' in css
    assert 'tests/e2e/test_mobile_detail_watch_release.py' in workflow


def test_detail_watch_ignores_a_late_subscription_response_for_the_previous_item():
    script = f"""
require({json.dumps(str(MODULE))});
const createDetailWatch = globalThis.createDetailWatch;
const state={{renders:[]}};
const completions={{}};
const feature=createDetailWatch({{
  fetchJson:path => new Promise(resolve => {{completions[path]=resolve;}}),
  onState:(status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{
  const first=feature.open({{repository:'stackchain/api',kind:'issue',number:1,state:'open'}});
  const second=feature.open({{repository:'stackchain/api',kind:'issue',number:2,state:'open'}});
  completions[Object.keys(completions)[1]]({{watching:true}});
  await second;
  completions[Object.keys(completions)[0]]({{watching:false}});
  await first;
  process.stdout.write(JSON.stringify(state));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
    result = json.loads(subprocess.run(
        ["node", "-e", script], text=True, capture_output=True, check=True
    ).stdout)

    ready = [state for state in result["renders"] if state["status"] == "ready"]
    assert len(ready) == 1
    assert ready[0]["watching"] is True


def test_detail_watch_reports_subscription_load_failure_as_recoverable_error():
    script = f"""
require({json.dumps(str(MODULE))});
const state={{renders:[]}};
const feature=globalThis.createDetailWatch({{
  fetchJson:async () => {{throw new Error('temporarily unavailable');}},
  onState:(status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{
  try {{ await feature.open({{repository:'stackchain/api',kind:'issue',number:1,state:'open'}}); }}
  catch (error) {{ state.rejected=error.message; }}
  process.stdout.write(JSON.stringify(state));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
    result = json.loads(subprocess.run(
        ["node", "-e", script], text=True, capture_output=True, check=True
    ).stdout)

    assert result["rejected"] == "temporarily unavailable"
    assert result["renders"][-1] == {
        "status": "error", "watching": False, "error": "temporarily unavailable"
    }
