import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


OFFLINE_TODAY = Path(__file__).parents[1] / "frontend" / "offline-today.js"


def run_scenario(scenario: str) -> dict:
    script = f"""
const createOfflineToday = require({json.dumps(str(OFFLINE_TODAY))});
(async () => {{
{scenario}
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
    result = subprocess.run(
        ["node", "-e", script], check=True, capture_output=True, text=True
    )
    return json.loads(result.stdout)


def test_warms_bounded_today_queue_with_two_concurrent_requests():
    result = run_scenario("""
let active = 0;
let peak = 0;
const loaded = [];
const saved = [];
const items = Array.from({length:7}, (_, index) => ({
  kind:index % 2 ? 'pull' : 'issue', repository:'stackchain/dashboard', number:index + 1,
  updated_at:'2026-08-08T12:0' + index + ':00Z',
}));
const warmer = createOfflineToday({
  loadDetail: async item => {
    active += 1;
    peak = Math.max(peak, active);
    loaded.push(item.number);
    await new Promise(resolve => setTimeout(resolve, 5));
    active -= 1;
    return {title:'Work ' + item.number, body:'Context ' + item.number};
  },
  loadSavedDetail: () => null,
  saveDetail: (login, item, detail) => saved.push({login, number:item.number, detail}),
});
const status = await warmer.warm('timmy', items);
process.stdout.write(JSON.stringify({peak, loaded, saved, status}));
""")

    assert result["peak"] == 2
    assert result["loaded"] == [1, 2, 3, 4, 5]
    assert [entry["number"] for entry in result["saved"]] == [1, 2, 3, 4, 5]
    assert result["saved"][0]["detail"]["source_updated_at"] == "2026-08-08T12:00:00Z"
    assert result["status"] == {"total": 5, "ready": 5, "failed": 0, "pending": 0}


def test_readiness_waits_for_durable_async_admission_and_reports_rejection():
    result = run_scenario("""
const events = [];
const warmer = createOfflineToday({
  loadDetail: async () => ({title:'Fetched detail'}),
  loadSavedDetail: async () => null,
  saveDetail: async () => {
    events.push('admission-started');
    await new Promise(resolve => setTimeout(resolve, 5));
    events.push('admission-rejected');
    return false;
  },
  onStatus: status => events.push('ready:' + status.ready + '/failed:' + status.failed),
});
const status = await warmer.warm('timmy', [{
  kind:'issue', repository:'stackchain/dashboard', number:1, updated_at:'v1',
}]);
process.stdout.write(JSON.stringify({events, status, failed:warmer.failedKeys()}));
""")

    assert result["status"] == {"total": 1, "ready": 0, "failed": 1, "pending": 0}
    assert result["failed"] == ["issue:stackchain/dashboard:1"]
    assert result["events"].index("admission-rejected") < result["events"].index(
        "ready:0/failed:1"
    )


def test_refreshes_only_changed_revisions_and_keeps_prior_copy_on_failure():
    result = run_scenario("""
const items = [
  {kind:'issue', repository:'stackchain/dashboard', number:1, updated_at:'new'},
  {kind:'pull', repository:'stackchain/dashboard', number:2, updated_at:'same'},
];
const old = new Map([
  ['issue:stackchain/dashboard:1', {title:'Prior 1', source_updated_at:'old'}],
  ['pull:stackchain/dashboard:2', {title:'Prior 2', source_updated_at:'same'}],
]);
const loaded = [];
const warmer = createOfflineToday({
  loadDetail: async item => { loaded.push(item.number); throw new Error('temporary outage'); },
  loadSavedDetail: (_login, item) => old.get(item.kind + ':' + item.repository + ':' + item.number),
  saveDetail: () => { throw new Error('must not replace a prior copy after failure'); },
});
const status = await warmer.warm('timmy', items);
process.stdout.write(JSON.stringify({loaded, status, failed:warmer.failedKeys()}));
""")

    assert result == {
        "loaded": [1],
        "status": {"total": 2, "ready": 2, "failed": 1, "pending": 0},
        "failed": ["issue:stackchain/dashboard:1"],
    }


def test_retry_fetches_only_failed_items():
    result = run_scenario("""
const items = [
  {kind:'issue', repository:'stackchain/dashboard', number:1, updated_at:'v2'},
  {kind:'issue', repository:'stackchain/dashboard', number:2, updated_at:'v1'},
];
let fail = true;
const calls = [];
const saved = new Map([['issue:stackchain/dashboard:2', {source_updated_at:'v1'}]]);
const warmer = createOfflineToday({
  loadDetail: async item => {
    calls.push(item.number);
    if (fail) throw new Error('offline');
    return {title:'Fresh'};
  },
  loadSavedDetail: (_login, item) => saved.get(item.kind + ':' + item.repository + ':' + item.number),
  saveDetail: (_login, item, detail) => saved.set(item.kind + ':' + item.repository + ':' + item.number, detail),
});
await warmer.warm('timmy', items);
fail = false;
const status = await warmer.retry('timmy', items);
process.stdout.write(JSON.stringify({calls, status}));
""")

    assert result["calls"] == [1, 1]
    assert result["status"] == {"total": 2, "ready": 2, "failed": 0, "pending": 0}


def test_requested_reviews_are_warmed_as_distinct_today_items():
    result = run_scenario("""
const loaded = [];
const saved = [];
const review = {
  kind:'pull', is_review:true, repository:'stackchain/dashboard', number:12,
  updated_at:'2026-08-08T12:00:00Z',
};
const warmer = createOfflineToday({
  loadDetail: async item => {
    loaded.push(item.is_review ? 'review' : item.kind);
    return {head_sha:'abc123', files:[{filename:'src/app.js'}]};
  },
  loadSavedDetail: () => null,
  saveDetail: (_login, item, detail) => saved.push({item, detail}),
});
const status = await warmer.warm('timmy', [review]);
process.stdout.write(JSON.stringify({loaded, saved, status}));
""")

    assert result["loaded"] == ["review"]
    assert result["saved"][0]["item"]["is_review"] is True
    assert result["saved"][0]["detail"]["head_sha"] == "abc123"
    assert result["status"] == {"total": 1, "ready": 1, "failed": 0, "pending": 0}


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

    assert '<script src="static/offline-today.js"></script>' in html
    assert 'id="offline-today-status"' in html
    assert 'id="retry-offline-today"' in html
    assert "offlineToday.warm(confirmedOwnerLogin, todayMyWork)" in html
    assert "offlineToday.retry(confirmedOwnerLogin, todayMyWork)" in html
    assert "Today offline: " in html
    assert ".offline-today-readiness button { min-height:44px;" in html
