import json
import subprocess
from pathlib import Path


SAVED_SEARCHES = Path(__file__).parents[1] / "frontend" / "saved-searches.js"


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


def test_saved_search_controller_loads_saves_opens_renames_and_deletes():
    script = f"""
const createSavedSearches=require({json.dumps(str(SAVED_SEARCHES))});
const requests=[]; const opened=[]; const states=[];
let remote={{revision:0,views:[]}}; let sequence=0;
const controller=createSavedSearches({{
 fetchJson:async (_url,options={{}})=>{{
   requests.push(options.method||'GET');
   if(!options.method)return remote;
   const body=JSON.parse(options.body);remote={{revision:remote.revision+1,views:body.views}};return remote;
 }},
 createId:()=> 'view-'+(++sequence),
 onOpen:view=>opened.push(view),
 onState:state=>states.push(state),
}});
(async()=>{{
 await controller.load();
 await controller.save('Release queue',{{query:'mobile',kind:'issue',state:'open',repository:'stackchain/dashboard'}});
 controller.open('view-1');
 await controller.rename('view-1','Morning release queue');
 await controller.remove('view-1');
 process.stdout.write(JSON.stringify({{requests,opened,snapshot:controller.snapshot(),statuses:states.map(x=>x.status)}}));
}})().catch(error=>{{console.error(error);process.exit(1)}});
"""
    assert run_node(script) == {
        "requests": ["GET", "PUT", "PUT", "PUT"],
        "opened": [{
            "id": "view-1",
            "name": "Release queue",
            "query": "mobile",
            "kind": "issue",
            "state": "open",
            "repository": "stackchain/dashboard",
        }],
        "snapshot": {"revision": 3, "views": []},
        "statuses": ["loading", "ready", "saving", "ready", "saving", "ready", "saving", "ready"],
    }


def test_saved_search_controller_adopts_conflict_and_keeps_search_usable_on_failure():
    script = f"""
const createSavedSearches=require({json.dumps(str(SAVED_SEARCHES))});
const states=[];let mode='conflict';
const server={{revision:4,views:[{{id:'remote',name:'Remote',query:'review',kind:'pull',state:'open',repository:''}}]}};
const controller=createSavedSearches({{
 fetchJson:async (_url,options={{}})=>{{
   if(!options.method)return {{revision:3,views:[]}};
   if(mode==='conflict'){{const error=new Error('conflict');error.status=409;error.payload={{detail:{{snapshot:server}}}};throw error;}}
   throw new Error('offline');
 }},createId:()=> 'local',onOpen:()=>{{}},onState:state=>states.push([state.status,state.message||'']),
}});
(async()=>{{
 await controller.load();
 try{{await controller.save('Local',{{query:'mobile',kind:'all',state:'all'}})}}catch(_error){{}}
 mode='offline';
 try{{await controller.remove('remote')}}catch(_error){{}}
 process.stdout.write(JSON.stringify({{snapshot:controller.snapshot(),states}}));
}})();
"""
    result = run_node(script)
    assert result["snapshot"] == {
        "revision": 4,
        "views": [{
            "id": "remote", "name": "Remote", "query": "review",
            "kind": "pull", "state": "open", "repository": "",
        }],
    }
    assert result["states"][-1] == ["error", "Saved searches could not sync. Search still works."]
    assert ["conflict", "Saved searches changed on another device."] in result["states"]


def test_mobile_search_renders_synced_saved_view_controls_and_runtime_wiring():
    from tests.dashboard_bundle import dashboard_bundle_text

    html = dashboard_bundle_text()
    css = (SAVED_SEARCHES.parent / "dashboard.css").read_text()
    dashboard = (SAVED_SEARCHES.parent / "dashboard.js").read_text()

    assert '<section class="saved-searches" aria-labelledby="saved-searches-heading">' in html
    assert 'id="saved-search-name"' in html
    assert 'id="save-current-search"' in html
    assert 'id="saved-search-list"' in html
    assert 'id="saved-search-status"' in html
    assert "createSavedSearches.mount(" in dashboard
    saved_searches = SAVED_SEARCHES.read_text()
    assert "search.setQuery('')" in saved_searches
    assert "applyScope(view)" in saved_searches
    assert "search.setQuery(view.query)" in saved_searches
    assert ").load();" in dashboard
    assert "static/saved-searches.js" in (SAVED_SEARCHES.parent.parent / "src" / "frontend_bundle.py").read_text()
    assert ".saved-search-action { min-height:44px;" in css
