import {linkShared} from './helpers/shared-modules.mjs';
import {routingModuleUrl} from './helpers/routing.mjs';
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import ts from 'typescript';
async function moduleUrl(file,replace=code=>code){
const filename=new URL(file,import.meta.url);
const {outputText}=ts.transpileModule(await readFile(filename,'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}});
return 'data:text/javascript;base64,'+Buffer.from(linkShared(replace(outputText)).replace(/(['"])\.\.\/routing\1/g,JSON.stringify(routingModuleUrl))+'\n//# sourceURL='+filename.pathname).toString('base64');
}
const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts');
const renderUrl=await moduleUrl('../src/lib/client/render.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)));
const searchUrl=await moduleUrl('../src/lib/client/search.ts',code=>code.replace("'../api/protocol'",JSON.stringify(protocolUrl)).replace("'./render'",JSON.stringify(renderUrl)));
const routingUrl=await moduleUrl('../src/lib/routing.ts');
const {loadSearchResults,searchResultsMarkup,searchErrorMarkup,searchSuggestionsMarkup,searchPicksMarkup,SEARCH_PAGE_SIZE}=await import(searchUrl);
const {highlightSearchTitle}=await import(renderUrl);
const {paginationPath,searchPath,parseSitePath}=await import(routingUrl);
const signal=()=>new AbortController().signal;
const drama={id:7,title:'SEO 标题',playName:'都市 <短剧>',cover:'https://cdn.example/encrypted.webp',totalEpisode:12};
test('search uses reference API parameters, authoritative totals and actual playName without local filtering',async()=>{
const calls=[];
const result=await loadSearchResults({get:async(path,query,requestSignal)=>{calls.push({path,query,requestSignal});return {keyword:'都市剧',list:[null,drama,{id:8,title:'服务端判定的相关作品'}],totalCount:102,pageSize:25};}},' 都市 ',3,signal());
assert.equal(SEARCH_PAGE_SIZE,25);assert.equal(calls.length,1);assert.equal(calls[0].path,'playlet/search');assert.deepEqual(calls[0].query,{keyword:'都市',pageNum:3,pageSize:25});
assert.equal(result.keyword,'都市剧');assert.equal(result.total,102);assert.equal(result.page,3);assert.equal(result.pageSize,25);
assert.equal(result.list[0].title,'都市 <短剧>');assert.equal(result.list.length,2);assert.deepEqual(result.recommended,[]);
});
test('independent search page links preserve encoded keywords and use the response page size',async()=>{
const keyword='都市 / 爱情?#%';
const result=await loadSearchResults({get:async()=>({list:[drama],totalCount:61,pageSize:10})},keyword,2,signal());
const base=searchPath(keyword,2);
const html=searchResultsMarkup(result,page=>paginationPath(base,page));
for(const page of [1,3,7])assert.ok(html.includes('href="'+searchPath(keyword,page)+'"'));
assert.ok(html.includes('name="page" min="1" max="7"'));assert.ok(html.includes('共 61 个结果'));assert.ok(html.includes('data-src="https://cdn.example/encrypted.webp"'));
assert.ok(!html.includes('SEO 标题'));assert.ok(html.includes('都市 <短剧>'));assert.ok(!html.includes('#search/'));
});
test('empty results request six hottest works and keep them separate from the search count',async()=>{
const calls=[];
const result=await loadSearchResults({get:async(path,query)=>{
calls.push({path,query});
if(path==='playlet/search')return {list:null,totalCount:0};
return {list:[null,{id:0,title:'无效'},drama,drama,...Array.from({length:7},(_,i)=>({...drama,id:i+10}))]};
}},'没有匹配的关键词',1,signal());
assert.deepEqual(calls[1],{path:'playlet/operate/list',query:{pageNum:1,pageSize:6,type:2}});
assert.equal(result.recommended.length,6);assert.equal(new Set(result.recommended.map(item=>item.id)).size,6);assert.equal(result.total,0);assert.deepEqual(result.list,[]);
const html=searchResultsMarkup(result,()=>assert.fail('empty first page has no pagination'));
assert.ok(html.includes('共 0 个结果'));assert.ok(html.includes('搜索无结果'));assert.ok(html.includes('f2ef01f956fdcd80.png'));
assert.ok(html.includes('大家都在看'));assert.ok(html.includes('aria-label="热门推荐"'));assert.ok(html.includes('href="/shorts/"'));
});
test('empty entry skips the search endpoint; unavailable recommendations never replace an empty result with an error',async()=>{
const calls=[];
const result=await loadSearchResults({get:async(path,query)=>{calls.push({path,query});throw new Error('recommendation unavailable');}},' ',1,signal());
assert.deepEqual(calls.map(call=>call.path),['playlet/operate/list']);assert.equal(result.keyword,'');assert.deepEqual(result.recommended,[]);
const html=searchResultsMarkup(result,()=>assert.fail('empty entry cannot paginate'));
assert.ok(html.includes('搜索你想看的短剧'));assert.ok(html.includes('输入剧名、演员或标签'));assert.ok(!html.includes('大家都在看'));assert.ok(!html.includes('recommendation unavailable'));
});
test('search failures have their own retry state and never masquerade as zero matches',async()=>{
const calls=[];
await assert.rejects(loadSearchResults({get:async path=>{calls.push(path);throw new Error('搜索服务超时');}},'原名',1,signal()),/搜索服务超时/);
assert.deepEqual(calls,['playlet/search']);
const html=searchErrorMarkup('