import assert from 'node:assert/strict';
import {DatabaseSync} from 'node:sqlite';
import {readFileSync,readdirSync} from 'node:fs';
import {env} from '../db/node-env';
import {makeSession} from '../lib/auth';
import {GET} from '../app/api/transfer-access/route';
import {checkExportAccess} from '../lib/transfer-access';
import {projectKey} from '../lib/projects';
import {claimProject} from '../lib/billing';
import type {Database} from '../db/database';
const sql=new DatabaseSync(':memory:');
for(const f of readdirSync('drizzle').filter(f=>f.endsWith('.sql')).sort())sql.exec(readFileSync('drizzle/'+f,'utf8').replaceAll('--> statement-breakpoint',''));
type Params=(string|number|null)[];
const db:Database={kind:'d1',async first<T>(s:string,p:Params=[]){return (sql.prepare(s).get(...p)||null) as T|null},async all<T>(s:string,p:Params=[]){return sql.prepare(s).all(...p) as T[]},async run(s,p=[]){return Number(sql.prepare(s).run(...p).changes)}};
Object.assign(env,{ADMIN_USER_ID:'admin',DB:{prepare(s:string){return {bind(...p:Params){return {first:()=>db.first(s,p),all:async()=>({results:await db.all(s,p)}),run:async()=>({meta:{changes:await db.run(s,p)}})}}}}}});
for(const id of ['buyer','other'])await db.run('INSERT INTO users (id,email,name,password_hash,created_at) VALUES (?,?,?,?,?)',[id,id+'@example.test',id,'unused',1]);
const cookie=async(userId:string)=>(await makeSession(db,userId,new Headers({host:'127.0.0.1:5180'}),0)).split(';')[0];
const buyerCookie=await cookie('buyer'),otherCookie=await cookie('other');
const req=(c:string|null,query:string)=>new Request('http://127.0.0.1:5180/api/transfer-access?'+query,{headers:c?{host:'127.0.0.1:5180',cookie:c}:{host:'127.0.0.1:5180'}});
const exp=(c:string|null,projectId:string)=>GET(req(c,'purpose=export&projectId='+encodeURIComponent(projectId)));
const A='11111111-1111-4111-8111-111111111111',B='22222222-2222-4222-8222-222222222222',C='33333333-3333-4333-8333-333333333333';
const plan=(id:string,state='active')=>db.run('INSERT INTO plans(id,data,revision,updated_at,state) VALUES (?,?,?,?,?)',[projectKey('buyer',id),'{}',1,'2026-01-0'+[ 'default',A,B,C].indexOf(id)+'T00:00:00Z',state]);
const code=async(r:Response)=>((await r.json()) as {code?:string}).code;
assert.equal((await exp(null,'default')).status,401);
assert.equal((await exp(buyerCookie,'../x')).status,400,'invalid project id');
let r=await exp(buyerCookie,'default');assert.equal(r.status,409);assert.equal(await code(r),'SAVE_REQUIRED','unsaved project without subscription');
assert.equal(await claimProject(db,{userId:'buyer',email:'buyer@example.test',displayName:'buyer'},'default'),true,'saving claims the free slot');await plan('default');
r=await exp(buyerCookie,'default');assert.equal(r.status,200,'first free project exports without subscription');const ok=await r.json() as {userId:string;projectId:string;export:boolean;via:string};assert.deepEqual([ok.userId,ok.projectId,ok.export,ok.via],['buyer','default',true,'project']);assert.match(r.headers.get('cache-control')||'',/no-store/);
assert.equal((await GET(req(buyerCookie,''))).status,402,'import stays subscription-only');
await plan(A);await db.run('INSERT INTO billing_grants (id,user_id,project_id,mode,created_at) VALUES (?,?,?,?,?)',['paid-a','buyer',projectKey('buyer',A),'live',2]);
assert.equal((await exp(buyerCookie,A)).status,200,'one-off purchased project exports without subscription');
await plan(B);await db.run('INSERT INTO billing_grants (id,user_id,project_id,mode,created_at) VALUES (?,?,?,?,?)',['sub-b','buyer',projectKey('buyer',B),'sub_live',3]);
await plan(C);
r=await exp(buyerCookie,B);assert.equal(r.status,402);assert.equal(await code(r),'SUBSCRIPTION_REQUIRED','subscription project needs subscription');
assert.equal((await exp(buyerCookie,C)).status,402,'ungranted legacy project needs subscription');
await db.run('INSERT INTO billing_grants (id,user_id,project_id,mode,created_at) VALUES (?,?,?,?,?)',['test-c','buyer',projectKey('buyer',C),'test',4]);
assert.equal((await exp(buyerCookie,C)).status,402,'test-mode purchase does not unlock live accounts');
await db.run('INSERT INTO billing_subscriptions (id,user_id,mode,order_id,customer_id,status,paid_until,cancel_at_period_end,revision,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)',['sub','buyer','live','order','customer','active',Date.now()+60000,0,1,1]);
for(const id of [B,C])assert.equal((await exp(buyerCookie,id)).status,200,'active subscription exports every own project');
r=await exp(buyerCookie,'44444444-4444-4444-8444-444444444444');assert.equal(r.status,200,'active subscription can export an unsaved new project');
assert.equal((await GET(req(buyerCookie,''))).status,200,'import with subscription');
await db.run('UPDATE billing_subscriptions SET paid_until = ?',[Date.now()-1]);
assert.equal((await exp(buyerCookie,B)).status,402,'expired subscription locks subscription projects');
assert.equal((await exp(buyerCookie,A)).status,200,'purchased project still exports after subscription expiry');
assert.equal((await exp(buyerCookie,'default')).status,200,'free project still exports after subscription expiry');
await db.run('UPDATE plans SET state = ? WHERE id = ?',['archived',projectKey('buyer',A)]);r=await exp(buyerCookie,A);assert.equal(r.status,409);assert.equal(await code(r),'PROJECT_INACTIVE');
await db.run('UPDATE plans SET state = ? WHERE id = ?',['trash',projectKey('buyer','default')]);assert.equal((await exp(buyerCookie,'default')).status,409,'trashed project');
await db.run('UPDATE plans SET state = ? WHERE id = ?',['active',projectKey('buyer','default')]);
r=await exp(otherCookie,A);assert.equal(r.status,409);assert.equal(await code(r),'SAVE_REQUIRED','another user only reaches their own key');
globalThis.fetch=async()=>exp(buyerCookie,'default');
assert.equal((await checkExportAccess('buyer','default')).allowed,true);
assert.equal((await checkExportAccess('other','default')).allowed,false,'switched account is rejected');
assert.equal((await checkExportAccess('buyer',A)).allowed,false,'response for another project is rejected');
globalThis.fetch=async()=>exp(buyerCookie,B);assert.equal((await checkExportAccess('buyer',B)).subscriptionRequired,true);
globalThis.fetch=async()=>{throw Error('offline')};assert.equal((await checkExportAccess('buyer','default')).allowed,false,'network failure fails closed');
console.log('PASS: per-project export: free and one-off projects without subscription (also after expiry), subscription/legacy/test-mode projects locked, unsaved/archived/trashed/foreign projects, import unchanged, account/project mismatch and offline fail closed.');
