from backend.public_enquiries import TABLES as INTAKE_TABLES
from backend.customer_review_models import TABLES as CUSTOMER_TABLES
"""Format 10 recovery and strict migrations; all data is fictional."""
import hashlib,json,sqlite3
from pathlib import Path
import pytest
from backend import recovery
from backend.app import create_app
from backend.workflow_recovery import TABLES
from test_boundaries import app,client,write,job
from test_workflow import edit,items,decide,config
from test_feedback import prepare,respond,publish,action,BASE,PUBLIC
from test_feedback_recovery import run_feedback_drill
from test_recovery import table_rows,new_login,rewrite_archive

def populate(application,c,j,twin):
    edit(c,j,priority='urgent',tags=['reviewed'],notes='Harmless private amendment after permission')
    r=write(c,'/api/customers/'+j['customer_id']+'/jobs',{'customer_version':1,'service':'Another invented task','message':'Keep original evidence and new work separate.'});assert r.status_code==200,r.text
    next_job=r.json();edit(c,next_job,next_action='Review the next invented job',next_action_date='2026-01-01',priority='high',tags=['repeat'])
    config(c,quote_days=12,repeat_days=120,excluded_tags=['no followup'])
    item=next(x for x in items(c,'next_action') if x['job_id']==next_job['id']);item=decide(c,item);item=decide(c,item,'complete');item=decide(c,item,'reopen');decide(c,item,'dismiss')

def run_workflow_drill(tmp_path):
    report=run_feedback_drill(tmp_path,populate_workflow=populate)
    report.update(operational_jobs_with_private_metadata=8,reviewed_next_action_sources=4,review_events=16,business_specific_settings=4,all_six_operational_tables_preserved=True,harmless_metadata_preserved_original_feedback_binding=True)
    return report

def test_four_business_operational_disappearance_and_restore(tmp_path):
    report=run_workflow_drill(tmp_path);(tmp_path/'workflow-recovery-drill.json').write_text(json.dumps(report,indent=2)+'\n')

@pytest.mark.parametrize('tamper',['missing_baseline','metadata_extra','metadata_without_history','feedback_basis','job_history_gap','foreign_actor','settings_history','review_history_gap','review_foreign_customer','review_foreign_job','review_context','review_source_version','review_action','review_draft','job_snapshot'])
def test_operational_restore_rejects_tampered_state_before_creating_target(app,tmp_path,tamper):
    c=client(app);j=job(c);other=job(c);edit(c,j,next_action='Review invented work',priority='urgent');config(c,quote_days=12);decide(c,items(c,'next_action')[0]);saved=tmp_path/'source.zip';recovery.backup(app.state.store.root,saved)
    def mutate(files,meta):
        name='businesses/oak/business.sqlite3';p=tmp_path/'changed.sqlite3';p.write_bytes(files[name])
        with sqlite3.connect(p) as db:
            if tamper=='missing_baseline':db.execute('DELETE FROM job_workflow WHERE job_id=?',(j['id'],))
            elif tamper in ('metadata_extra','metadata_without_history'):
                v=json.loads(db.execute('SELECT content FROM job_workflow WHERE job_id=?',(j['id'],)).fetchone()[0]);v['public' if tamper=='metadata_extra' else 'priority']='high';db.execute('UPDATE job_workflow SET content=? WHERE job_id=?',(json.dumps(v),j['id']))
            elif tamper=='feedback_basis':db.execute('UPDATE job_workflow SET feedback_version=2 WHERE job_id=?',(j['id'],))
            elif tamper=='job_history_gap':db.execute('DELETE FROM job_workflow_history WHERE job_id=? AND version=1',(j['id'],))
            elif tamper=='foreign_actor':db.execute("UPDATE opportunity_history SET actor='harbour'")
            elif tamper=='settings_history':db.execute('DELETE FROM opportunity_settings_history')
            elif tamper=='review_history_gap':db.execute('DELETE FROM opportunity_history')
            elif tamper=='review_foreign_customer':db.execute('UPDATE opportunity_reviews SET customer_id=?',(other['customer_id'],))
            elif tamper=='review_foreign_job':db.execute('UPDATE opportunity_reviews SET job_id=?',(other['id'],))
            elif tamper in ('review_context','review_source_version'):
                v=json.loads(db.execute('SELECT context FROM opportunity_reviews').fetchone()[0]);v['source_version' if tamper=='review_source_version' else 'business_id']=999;db.execute('UPDATE opportunity_reviews SET context=?',(json.dumps(v),))
            elif tamper=='review_action':db.execute("UPDATE opportunity_history SET action='complete'")
            elif tamper=='review_draft':db.execute("UPDATE opportunity_reviews SET draft='Rewritten without an event'")
            elif tamper=='job_snapshot':db.execute("UPDATE jobs SET notes='Unrecorded overwrite' WHERE id=?",(j['id'],))
        files[name]=p.read_bytes();meta['files'][name].update(bytes=len(files[name]),sha256=hashlib.sha256(files[name]).hexdigest())
    bad=tmp_path/'changed.zip';sha=rewrite_archive(saved,bad,mutate);target=tmp_path/'restored'
    with pytest.raises((recovery.RecoveryError,ValueError,KeyError)):recovery.restore(bad,sha,target,confirm_fresh=True)
    assert not target.exists()

def strip_operational(root):
    for bid in ('oak','harbour'):
        with sqlite3.connect(root/bid/'business.sqlite3') as db:
            for table in INTAKE_TABLES+CUSTOMER_TABLES+TABLES:db.execute('DROP TABLE '+table)

def test_in_place_format_nine_migration_keeps_active_and_revoked_permission_exact(tmp_path):
    root=tmp_path/'old';application=create_app(root);c=client(application);j,rid,code=prepare(c);respond(c,application,rid,code);publish(c,rid)
    other,revoked,code2=prepare(c);respond(c,application,revoked,code2);publish(c,revoked);action(c,revoked,'revoke')
    before=table_rows(root,'oak');strip_operational(root);migrated=create_app(root);fresh=client(migrated);after=table_rows(root,'oak')
    for table in ('feedback_requests','feedback_responses','feedback_consents','feedback_publications','feedback_history','feedback_tokens'):assert before[table]==after[table]
    assert fresh.get(PUBLIC).json()['count']==1;edit(fresh,j,notes='Private migrated note',priority='high');assert fresh.get(PUBLIC).json()['count']==1
    assert fresh.get(BASE+revoked).json()['request']['state']=='revoked'
    assert all(json.loads(r['content'])['completed_date']=='' for r in after['job_workflow'])
    saved=recovery.backup(root,tmp_path/'after-migration.zip');assert recovery.verify(saved['archive'],saved['sha256'])['valid']

def test_format_nine_archive_migrates_and_never_revives_old_publication(tmp_path):
    root=tmp_path/'old';application=create_app(root);c=client(application);j,rid,code=prepare(c);respond(c,application,rid,code);publish(c,rid);original=tmp_path/'original.zip';recovery.backup(root,original)
    def mutate(files,meta):
        meta['version']=9;meta['prototype_version']='0.12.0'
        for name,data in list(files.items()):
            if not name.startswith('businesses/') or not name.endswith('.sqlite3'):continue
            p=tmp_path/(name.split('/')[1]+'.sqlite3');p.write_bytes(data)
            with sqlite3.connect(p) as db:
                for table in INTAKE_TABLES+CUSTOMER_TABLES+TABLES:db.execute('DROP TABLE '+table)
            files[name]=p.read_bytes();meta['files'][name].update(bytes=len(files[name]),sha256=hashlib.sha256(files[name]).hexdigest())
    old=tmp_path/'v9.zip';sha=rewrite_archive(original,old,mutate);assert recovery.verify(old,sha)['valid'];target=tmp_path/'restored';recovery.restore(old,sha,target,confirm_fresh=True);new=create_app(target);fresh,_=new_login(new,target,'oak')
    assert fresh.get(PUBLIC).json()['count']==0;assert fresh.get(BASE+rid).json()['request']['state']=='revoked';assert fresh.get('/api/opportunities').json()['items']==[]
    saved=recovery.backup(target,tmp_path/'second.zip');assert recovery.verify(saved['archive'],saved['sha256'])['valid']
