import fs from 'node:fs';
import {expect,test} from '@playwright/test';
import {adminEnv,storeEnv,root,recovery,labels,names,presetIds,keys,preset,selected,openPreset,save,go,settled,screen,fresh,record,blocked,denied,persist,homeSnapshot,type Role,type Check} from './permission-support';

process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY='1';
test.describe.configure({mode:'default'});
for(const role of ['CO','BO','VE'] as Role[])test(`[${role}] ADMIN setup → 4조합 실제 UI → 원상복구`,async({browser},info)=>{
 test.setTimeout(25*60_000);test.skip(process.env.QA_ALLOW_PERMISSION_MUTATION!=='1','BLOCKED: mutation guard missing');
 const rows:Check[]=[];const c=await browser.newContext({storageState:adminEnv().storageState,viewport:{width:1600,height:1000}});const a=await c.newPage();a.setDefaultTimeout(15000);
 let original:any;let store=await fresh(browser,role);let stage='baseline';const modifiedLabels=new Set<string>();
 const checks=(comb:string,ids:string|string[],ok:boolean,detail:string,evidence:string[]=[])=>{record(rows,role,comb,ids,ok,detail,evidence);persist(role,rows);};
 try{
  await go(store,'/');const baseHome=await screen(store,info,`${role}-baseline-home`);const baseSummary=await homeSnapshot(store);fs.mkdirSync(recovery,{recursive:true});fs.writeFileSync(`${recovery}/${role}-home-baseline.json`,JSON.stringify(baseSummary,null,2));
  const user=JSON.parse(fs.readFileSync(`${recovery}/${role}-user-detail.json`,'utf8')).payload;
  expect(user.rolePermissionPresetId).toBe(presetIds[role]);original=await preset(c,role);expect(original.name).toBe(names[role]);
  fs.mkdirSync(recovery,{recursive:true});const recoveryFile=`${recovery}/${role}-baseline.json`;if(fs.existsSync(recoveryFile))expect(selected(original),'prior baseline must still match').toEqual(selected(JSON.parse(fs.readFileSync(recoveryFile,'utf8'))));else fs.writeFileSync(recoveryFile,JSON.stringify(original,null,2));
  await openPreset(a,role);const before=await screen(a,info,`${role}-preset-before`);checks('baseline','P01',true,`실할당 ${names[role]} (#${presetIds[role]}), 전체 권한 ${selected(original).length}개를 원복 파일에 저장. 거래=${selected(original).includes(keys['스토어 거래조회'])}, 정산=${selected(original).includes(keys['스토어 정산조회'])}`,[before,baseHome]);
  let bothMonth:any;let bothRows:any;let sameRows:any;const combos=[[true,true,'거래+정산'],[true,false,'거래만'],[false,true,'정산만'],[false,false,'둘 다 없음']] as const;
  for(const [tx,st,comb]of combos){
   stage=comb;console.log(`[4298] ${role}: ${comb}`);await openPreset(a,role);for(const n of ['스토어 거래조회','스토어 정산조회'])modifiedLabels.add(n);
   await save(a,role,{'스토어 거래조회':tx,'스토어 정산조회':st},comb);
   const current=selected(await preset(c,role));const expected=selected(original).filter(x=>![keys['스토어 거래조회'],keys['스토어 정산조회']].includes(x));if(tx)expected.push(keys['스토어 거래조회']);if(st)expected.push(keys['스토어 정산조회']);expect(current).toEqual(expected.sort());
   const adminShot=await screen(a,info,`${role}-${tx?1:0}${st?1:0}-saved`);
   await store.reload({waitUntil:'domcontentloaded'});await settled(store);await go(store,'/');
   const reloadShot=await screen(store,info,`${role}-${tx?1:0}${st?1:0}-reload-home`);
   const reloadOk=(await store.getByText('금일매출',{exact:true}).count()>0)===tx&&(await store.getByText('정산금',{exact:true}).count()>0)===st;
   checks(comb,'P02',true,'독립 체크박스를 UI에서 저장하고 전체 권한 ID 집합으로 다른 권한 보존 확인',[adminShot]);
   checks(comb,['P03','C01','C02'].filter(x=>x==='P03'||x==='C01'&&!st||x==='C02'&&st),reloadOk,'UI 저장 → 기존 스토어 브라우저 새로고침 → 홈 표시 확인',[adminShot,reloadShot]);
   await store.context().close();store=await fresh(browser,role);await go(store,'/');const home=await screen(store,info,`${role}-${tx?1:0}${st?1:0}-fresh-home`);
   const sales=(await store.getByText('금일매출',{exact:true}).count()>0)&&(await store.getByText('전일매출',{exact:true}).count()>0);const settlement=(await store.getByText('정산금',{exact:true}).count())===2;const pending=await store.getByText('총 정산예정금',{exact:true}).count()>0;
   checks(comb,'P03',sales===tx&&settlement===st&&pending===st,'새 로그인 후 홈 항목·금액 표시 권한 일치',[home]);
   checks(comb,tx&&st?'H01':tx?'H02':st?'H04':'H05',sales===tx&&settlement===st&&pending===st,'금일·전일 매출 / 정산금 / 총 정산예정금 표시 비교',[home]);
   if(tx&&!st)checks(comb,'H03',!settlement&&!pending,'정산 항목명·금액 미표시',[home]);
   if(!st)checks(comb,'C01',!settlement&&!pending,'재로그인 후에도 정산 미표시',[home]);
   await store.getByRole('button',{name:'사이드바 열기'}).click();const nav=store.getByRole('navigation',{name:'사이드바 메뉴'});await nav.waitFor();const menu=await screen(store,info,`${role}-${tx?1:0}${st?1:0}-menu`);
   checks(comb,'N01',(await nav.getByRole('link',{name:'거래내역',exact:true}).count()>0)===tx&&(await nav.getByRole('link',{name:'정산내역',exact:true}).count()>0)===st,'사이드 메뉴 각각의 새 권한으로 제어',[menu]);
   await store.getByRole('button',{name:'사이드바 닫기',exact:true}).last().click();
   const history=store.getByRole('navigation',{name:'하단 메뉴'}).getByRole('link',{name:'내역',exact:true});const historyHref=await history.getAttribute('href').catch(()=>null);
   if(tx||st)checks(comb,'N03',historyHref===(tx?'/transactions':'/settlement'),`내역 목적지 ${historyHref}`,[home]);
   else checks(comb,'N04',role==='VE'?historyHref==='/link-shop-orders'||historyHref==='/link-payment-trackings':historyHref===null,`거래·정산 없음, 기존 권한 유지 시 내역 목적지 ${historyHref}`,[home]);
   if(!tx){const d=await denied(store,'/transactions',info,`${role}-${st?'01':'00'}-transaction-denied`);checks(comb,['N05','P04'],d.ok,'직접 주소 접근 제한 후 홈 이동; 기존 READ_TRANSACTION 권한은 유지',[d.shot]);}
   else{
    const responses:any[]=[];const listener=async(r:any)=>{if(r.url().includes('/store/transactions')&&r.request().method()==='GET')responses.push({path:new URL(r.url()).pathname,body:await r.json().catch(()=>null)});};store.on('response',listener);
    await go(store,'/transactions');const shot=await screen(store,info,`${role}-${st?'11':'10'}-transactions-today`);const body=await store.locator('body').innerText();
    checks(comb,'T01',/총 [\d,]+건/.test(body)&&body.includes('매출합계'),'거래내역 접근·총 건수·매출합계 표시',[shot]);
    checks(comb,st?'T02':'T03',st?body.includes('매출합계 (정산합계)'):body.includes('매출합계')&&!body.includes('정산합계')&&!/\([\d,−-]+원\)/.test(body),'거래 합계 정산 제목·괄호 표시',[shot]);
    const tabs=store.getByRole('navigation',{name:'내역 이동 탭'});checks(comb,'N02',st?await tabs.getByRole('link',{name:'정산내역',exact:true}).count()>0:await tabs.getByRole('link',{name:'정산내역',exact:true}).count()===0,'내역 상단 탭 권한 확인',[shot]);
    let filterOk=true;const counts:any[]=[];
    for(const period of ['전일','당월','전월','당일']){
     await store.getByRole('button',{name:period,exact:true}).click();await store.getByRole('button',{name:'조회',exact:true}).click();await settled(store);const text=await store.locator('body').innerText();filterOk&&=st?text.includes('정산합계'):!text.includes('정산합계');const s=await screen(store,info,`${role}-${st?'11':'10'}-${period}`);counts.push({period,text:text.slice(text.indexOf('조회 기간')),s});
    }
    await store.locator('input').nth(0).fill('2026-10-01');await store.locator('input').nth(1).fill('2026-10-19');await store.getByRole('button',{name:'조회',exact:true}).click();await settled(store);const month=await screen(store,info,`${role}-${st?'11':'10'}-custom-month`);const monthText=await store.locator('body').innerText();filterOk&&=st?monthText.includes('정산합계'):!monthText.includes('정산합계');checks(comb,['T07','R05'],filterOk,'당일·전일·당월·전월·직접 기간 조회 후 숨김 기준 확인',[shot,...counts.map(x=>x.s),month]);
    const txResponse=responses.filter(x=>x.path==='/api/v1/store/transactions').at(-1)?.body?.payload;const totalResponse=responses.filter(x=>x.path.endsWith('/total')).at(-1)?.body?.payload;
    const content=txResponse?.items?.content??txResponse?.content??[];if(st){bothMonth=totalResponse;bothRows=content;}else{sameRows=content;checks(comb,'T06',JSON.stringify(totalResponse)===JSON.stringify(bothMonth)&&JSON.stringify(content)===JSON.stringify(bothRows),'동일 직접 기간의 원본 거래 목록·합계 응답 불변 비교',[month]);}
    if(content.length){checks(comb,st?'T04':'T05',st?monthText.includes('원)'):!monthText.includes('원)'),`실제 목록 ${content.length}건의 정산 괄호 표시 확인 (승인·취소 포함 여부는 데이터 기록 참고)`,[month]);}else blocked(rows,role,st?'T04':'T05','직접 기간 조회 거래 0건: 건별 승인·취소 금액 비교 불가',[month]);
    blocked(rows,role,'T09','0원·음수 정산 및 승인·취소를 모두 갖춘 거래의 화면 대조 데이터 확보 필요',[month]);
    const more=store.getByRole('button',{name:/더 보기|더보기|추가 조회/});if(await more.count()) {await more.last().click();await settled(store);const moreShot=await screen(store,info,`${role}-${st?'11':'10'}-more`);checks(comb,'T08',st||(await store.locator('body').innerText()).includes('정산합계')===false,'추가 목록 조회 후 표시 확인',[moreShot]);}else blocked(rows,role,'T08','추가 조회 대상 목록/버튼 없음',[month]);
    const articles=store.locator('article');if(role==='VE')checks(comb,'R04',!monthText.includes('영업점')&&!monthText.includes('가맹점\n전체'),'가맹은 부서 선택 필터 없음',[month]);
    fs.writeFileSync(`${root}/matrix/${role}-${st?'11':'10'}-transactions.json`,JSON.stringify({total:totalResponse,rows:content,counts},null,2));store.off('response',listener);
   }
   if(!st){const d=await denied(store,'/settlement',info,`${role}-${tx?'10':'00'}-settlement-denied`);checks(comb,['N06','P04'],d.ok,'정산 직접 접근 제한 후 홈 이동; 기존 READ_SETTLEMENT 유지',[d.shot]);await store.goBack();await settled(store);checks(comb,'N07',!await store.getByText('정산합계',{exact:true}).isVisible(),'뒤로가기에서 제한된 정산 합계 미표시',[await screen(store,info,`${role}-${tx?'10':'00'}-back`)]);}
   else{await go(store,'/settlement');await store.getByRole('button',{name:'당월',exact:true}).click();await store.getByRole('button',{name:'조회',exact:true}).click();await settled(store);const s=await screen(store,info,`${role}-${tx?'11':'01'}-settlement`);const t=await store.locator('body').innerText();checks(comb,tx?'W01':['W01','W02'],new URL(store.url()).pathname==='/settlement'&&t.includes('합계')&&/총 [\d,]+건/.test(t),'정산 월 기간·목록·합계 화면 접근',[s]);}
   await go(store,'/wallets');const wallet=await screen(store,info,`${role}-${tx?1:0}${st?1:0}-wallets`);const walletText=await store.locator('body').innerText();
   if(!st)checks(comb,'W03',!walletText.includes('정산예정금')&&await store.getByRole('button',{name:'더보기',exact:true}).count()===0,'지갑 화면 정산예정금·펼치기 미표시',[wallet]);
   if(selected(original).includes('STORE_READ_REMITTANCE'))checks(comb,'W04',new URL(store.url()).pathname==='/wallets'&&walletText.includes('지갑잔고'),'지갑 별도 권한 유지 시 잔고·입출금 접근',[wallet]);
   await store.setViewportSize({width:390,height:844});await go(store,'/');const mobile=await screen(store,info,`${role}-${tx?1:0}${st?1:0}-mobile`);const overflow=await store.evaluate(()=>document.documentElement.scrollWidth>innerWidth);checks(comb,'C10',!overflow,'390px 모바일 가로 넘침 없음; 카드·버튼 겹침은 캡처 수동 검토',[mobile]);await store.setViewportSize({width:1280,height:900});
  }
  stage='sales-tally';
  if(role!=='VE'){
   await openPreset(a,role);modifiedLabels.add('매출집계 조회');await save(a,role,{'스토어 거래조회':true,'스토어 정산조회':true,'매출집계 조회':true},'매출집계 비교 기준');await store.reload();await settled(store);await store.context().close();store=await fresh(browser,role);await go(store,'/sales-summary');const tallyBefore=await store.locator('body').innerText();const tallyShot=await screen(store,info,`${role}-sales-tally-before`);
   await openPreset(a,role);await save(a,role,{'스토어 거래조회':true,'스토어 정산조회':false,'매출집계 조회':true},'매출집계 독립 권한 ON');await store.reload();await settled(store);await store.context().close();store=await fresh(browser,role);await go(store,'/sales-summary');const s=await screen(store,info,`${role}-sales-tally-on`);const t=await store.locator('body').innerText();checks('거래만+매출집계','C07',new URL(store.url()).pathname==='/sales-summary'&&t.includes('정산')&&t.includes('수익')&&t===tallyBefore,'정산 조회 ON/OFF 외 동일 조건에서 매출집계 원래 표시 전체 일치',[tallyShot,s]);
   await openPreset(a,role);await save(a,role,{'스토어 거래조회':true,'스토어 정산조회':true,'매출집계 조회':false},'매출집계 독립 권한 OFF');await store.reload();await settled(store);await store.context().close();store=await fresh(browser,role);const d=await denied(store,'/sales-summary',info,`${role}-sales-tally-off`);checks('거래+정산/매출집계 OFF','C08',d.ok,'거래·정산 권한으로 매출집계 직접 접근 불가',[d.shot]);
  }
  for(const [id,reason]of Object.entries({H06:'한쪽 항목 표시 화면의 여백·구분선 캡처 검토 필요',H07:'홈 0원 표시는 관찰; 음수 홈 정산 데이터 없음',H08:'무거래 날짜 확인; 실제 조회 실패 후 복구 재현 데이터 없음',R01:'법인 개별 부서 선택·합계 데이터 대조 추가 필요',R02:'법인 영업점 변경 후 가맹점 선택 초기화 추가 필요',R03:'영업점 고정 및 가맹점별 결과 대조 추가 필요',W05:'송금 실행에 사용할 독립 테스트 수취인·금액 미정',W06:'정산 추가와 지갑·송금 별도 OFF 조합 추가 필요',W07:'복수 지갑 전환 데이터 확보 필요',W08:'서버에 실제 적용된 지갑 방식 외 다른 방식 데이터 확보 필요',C03:'실서버 지연 조회 재현 조건 미확보',C04:'동일 브라우저 계정 전환 추가 검증 필요',C05:'취소 가능한 독립 테스트 거래 미확보',C06:'결제·주문·링크발송 기존 권한 화면 추가 확인 필요',C09:'비스킷카드 제외. 송금 수수료 화면 추가 확인 필요'})){
   if((id==='R01'||id==='R02')&&role!=='CO'||id==='R03'&&role!=='BO'||id==='C06'&&role!=='VE')continue;blocked(rows,role,id,reason);
  }
 }catch(error){blocked(rows,role,'P03',`${stage}: ${String(error)}`);await screen(a,info,`${role}-automation-error-admin`).catch(()=>{});await screen(store,info,`${role}-automation-error-store`).catch(()=>{});if(/Timeout|ERR_CONNECTION|ERR_HTTP2/.test(String(error)))throw new Error(`BLOCKED: ${stage}: ${String(error)}`);throw error;
 }finally{
  try{if(original){await openPreset(a,role);const restore=Object.fromEntries([...modifiedLabels].map(label=>[label,selected(original).includes(keys[label])]));await save(a,role,restore,'전체 원본 상태 복구');expect(selected(await preset(c,role)),'full permission ID restoration').toEqual(selected(original));const as=await screen(a,info,`${role}-preset-restored`);await store.reload();await settled(store);const refreshed=await screen(store,info,`${role}-restored-refresh`);await store.context().close();store=await fresh(browser,role);await go(store,'/');const ss=await screen(store,info,`${role}-restored-home`);const ids=selected(original);const ok=(await store.getByText('금일매출',{exact:true}).count()>0)===ids.includes('STORE_READ_TRANSACTION')&&(await store.getByText('정산금',{exact:true}).count()>0)===ids.includes('STORE_READ_SETTLEMENT');checks('restore',['F01','F02','F03','F04'],ok,'전체 원본 권한 ID 집합 일치; UI 저장·스토어 새로고침·새 로그인 후 원래 홈 표시 복원',[as,refreshed,ss]);fs.writeFileSync(`${recovery}/${role}-restored.json`,JSON.stringify({at:new Date().toISOString(),matched:true,permissionIds:ids},null,2));}}
  finally{persist(role,rows);await store.context().close();await c.close();}
 }
});
