from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta
import uuid
from fastapi.testclient import TestClient
import pytest
from backend.app import create_app
from backend.workforce import today_london
from test_boundaries import app, client, write, job, quote, invoice, portal, ORIGIN
from test_materials import material


def person(c, **changes):
    r=write(c,'/api/team',{'name':'Jamie Demo','role':'Installer','hourly_cost':'20.00','colour':'#245648',**changes})
    assert r.status_code==200,r.text
    return r.json()


def time_payload(j,p,**changes):
    return {'job_id':j['id'],'resource_id':p['id'],'resource_version':p['version'],'work_date':today_london().isoformat(),'duration_minutes':90,'note':'Private invented labour note',**changes}


def entry(c,j,p,**changes):
    r=write(c,'/api/time',time_payload(j,p,**changes));assert r.status_code==200,r.text
    return r.json()


def correction(e,**changes):
    return {'version':e['version'],'work_date':e['work_date'],'duration_minutes':e['duration_minutes'],'hourly_cost':f"{e['hourly_cost_pence']/100:.2f}",'note':e['note'],**changes}


def test_rate_snapshot_exact_rounding_corrections_and_history(app):
    c=client(app);j=job(c);p=person(c);e=entry(c,j,p)
    assert e['cost_pence']==3000 and e['resource_snapshot']['hourly_cost_pence']==2000
    assert entry(c,j,p,duration_minutes=1,hourly_cost_override='0.30')['cost_pence']==1
    updated=write(c,'/api/team/'+p['id'],{'version':1,'name':'Jamie Updated','role':'Lead','hourly_cost':'25.00','colour':'#000000'},method='put')
    assert updated.status_code==200
    saved=c.get('/api/jobs/'+j['id']+'/time').json()[-1]
    assert saved['resource_snapshot']['name']=='Jamie Demo' and saved['hourly_cost_pence']==2000
    assert write(c,'/api/time',time_payload(j,p)).status_code==409
    corrected=write(c,'/api/time/'+e['id'],correction(e,duration_minutes=120),method='put')
    assert corrected.status_code==200 and corrected.json()['cost_pence']==4000
    assert corrected.json()['resource_snapshot']==e['resource_snapshot']
    assert write(c,'/api/time/'+e['id'],correction(e),method='put').status_code==409
    history=c.get('/api/time/'+e['id']+'/history').json()
    assert len(history)==2 and history[0]['before_value']['cost_pence']==3000
    assert history[0]['after_value']['version']==2


@pytest.mark.parametrize('changes',[{'duration_minutes':True},{'duration_minutes':1.5},{'duration_minutes':0},{'duration_minutes':1441},{'work_date':'2026-02-30'},{'work_date':'2019-12-31'},{'work_date':(today_london()+timedelta(days=1)).isoformat()},{'hourly_cost_override':'-1'},{'hourly_cost_override':'NaN'},{'hourly_cost_override':'0.001'},{'business_id':'harbour'}])
def test_invalid_time_cannot_write(app,changes):
    c=client(app);j=job(c);p=person(c)
    assert write(c,'/api/time',time_payload(j,p,**changes)).status_code==422
    assert c.get('/api/jobs/'+j['id']+'/time').json()==[]


def test_capacity_across_jobs_archive_and_restore(app):
    c=client(app);j=job(c);j2=job(c);p=person(c);e=entry(c,j,p,duration_minutes=1400)
    assert write(c,'/api/time',time_payload(j2,p,duration_minutes=41)).status_code==409
    assert write(c,'/api/time/'+e['id']+'/archive',{'version':1}).status_code==200
    e2=entry(c,j2,p,duration_minutes=60)
    assert write(c,'/api/time/'+e['id']+'/restore',{'version':2}).status_code==409
    assert write(c,'/api/time/'+e2['id']+'/archive',{'version':1}).status_code==200
    assert write(c,'/api/time/'+e['id']+'/restore',{'version':2}).status_code==200
    assert len(c.get('/api/jobs/'+j2['id']+'/time?archived=true').json())==1
    assert write(c,'/api/team/'+p['id']+'/archive',{'version':1}).status_code==200
    assert write(c,'/api/time',time_payload(j2,{**p,'version':2})).status_code==409
    assert c.get('/api/jobs/'+j['id']+'/costs').json()['labour']['minutes']==1400


def test_time_retry_and_simultaneous_capacity_checks(app):
    c=client(app);j=job(c);p=person(c);key=str(uuid.uuid4());payload=time_payload(j,p,duration_minutes=800)
    first=write(c,'/api/time',payload,key=key)
    assert first.status_code==200 and write(c,'/api/time',payload,key=key).json()==first.json()
    write(c,'/api/time/'+first.json()['id']+'/archive',{'version':1})
    a=client(app);b=client(app)
    with ThreadPoolExecutor(2) as pool:
        results=list(pool.map(lambda cc:write(cc,'/api/time',payload).status_code,[a,b]))
    assert sorted(results)==[200,409]


def test_costs_keep_material_stages_invoices_payments_separate(app):
    c=client(app);j=job(c);p=person(c);e=entry(c,j,p)
    material(c,j,quantity='1',cost_price='12',status='received',ordered_date='2026-10-01',received_date='2026-10-02')
    material(c,j,quantity='2',cost_price='5')
    q=quote(c,j);i=invoice(c,q);write(c,'/api/invoices/'+i['id']+'/payments',{'amount_pence':2500})
    before=c.get('/api/documents/invoice/'+i['id']).json()
    costs=c.get('/api/jobs/'+j['id']+'/costs').json()
    assert costs['recorded_work_cost_pence']==4200 and costs['including_planned_materials_pence']==5200
    assert costs['invoiced_value_pence']==10000 and costs['recorded_payments_pence']==2500
    write(c,'/api/time/'+e['id']+'/archive',{'version':1})
    assert c.get('/api/jobs/'+j['id']+'/costs').json()['recorded_work_cost_pence']==1200
    assert c.get('/api/documents/invoice/'+i['id']).json()==before


def test_workforce_business_portal_and_csrf_boundaries(app):
    c=client(app);b=client(app,1);j=job(c);jb=job(b);p=person(c);pb=person(b);e=entry(c,j,p);guest=TestClient(app,headers=ORIGIN);pc,_=portal(app,c,j)
    for denied in (guest,pc):
        for path in ('/api/team','/api/jobs/'+j['id']+'/time','/api/jobs/'+j['id']+'/costs','/api/time/'+e['id']+'/history'):
            assert denied.get(path).status_code==401
        assert write(denied,'/api/time',time_payload(j,p)).status_code==401
    for path in ('/api/jobs/'+j['id']+'/time','/api/jobs/'+j['id']+'/costs','/api/time/'+e['id']+'/history'):
        assert b.get(path).status_code==404
    assert write(b,'/api/time',time_payload(j,pb)).status_code==404
    assert write(b,'/api/time',time_payload(jb,p)).status_code==404
    assert write(b,'/api/time/'+e['id'],correction(e),method='put').status_code==404
    assert write(b,'/api/time/'+e['id']+'/archive',{'version':1}).status_code==404
    assert 'Private invented labour note' not in pc.get('/api/portal/oak-and-vale/me').text
    c.headers['x-csrf-token']=''
    assert write(c,'/api/time',time_payload(j,p)).status_code==403


def test_time_persists_restart_and_blocks_customer_relink(app):
    from test_customers import contact
    c=client(app);j=job(c);p=person(c);e=entry(c,j,p);target=contact(c)
    assert write(c,'/api/jobs/'+j['id']+'/customer-link-preview',{'job_version':1,'customer_id':target['id'],'customer_version':1}).status_code==410
    restarted=client(create_app(app.state.store.root))
    assert restarted.get('/api/jobs/'+j['id']+'/time').json()[0]=={**e,'source_kind':'manual'}
