from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
import uuid
import pytest
from fastapi.testclient import TestClient
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_workforce import person


def payload(j,p,**changes):
    return {'kind':'job','job_id':j['id'],'resource_ids':[p['id']],'start_date':'2026-11-02','end_date':'2026-11-02','start_time':'09:00','end_time':'11:00','note':'Private invented booking note',**changes}


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


def edit_payload(row,**changes):
    keys=('kind','job_id','resource_ids','whole_team','all_day','title','start_date','end_date','start_time','end_time','start_fold','end_fold','note','version')
    result={k:row[k] for k in keys}
    for k in ('whole_team','all_day'):result[k]=bool(result[k])
    return {**result,**changes}


def listing(c,**query):
    return c.get('/api/calendar',params={'start_date':'2026-11-01','end_date':'2026-11-08',**query})


def test_interval_adjacency_different_people_and_multi_day_overlap(app):
    c=client(app);j=job(c);p=person(c);p2=person(c,name='Sam Demo')
    booking(c,j,p)
    assert write(c,'/api/calendar',payload(j,p,start_time='10:30',end_time='12:00')).status_code==409
    booking(c,j,p,start_time='11:00',end_time='12:00')
    booking(c,j,p2)
    booking(c,j,p,start_date='2026-11-03',end_date='2026-11-05',start_time='09:00',end_time='17:00')
    assert write(c,'/api/calendar',payload(j,p,start_date='2026-11-04',end_date='2026-11-04')).status_code==409
    assert len(listing(c).json()['items'])==4
    assert len(listing(c,resource_id=p2['id']).json()['items'])==1


def test_whole_team_unavailability_blocks_existing_and_new_people(app):
    c=client(app);j=job(c);p=person(c)
    b=booking(c,j,p,kind='unavailable',job_id=None,title='Workshop closed',whole_team=True,resource_ids=[],all_day=True,start_time='',end_time='')
    p2=person(c,name='New Person')
    for member in (p,p2):
        assert write(c,'/api/calendar',payload(j,member)).status_code==409
        assert listing(c,resource_id=member['id']).json()['items'][0]['id']==b['id']
    booking(c,j,p,start_date='2026-11-03',end_date='2026-11-03')
    assert write(c,'/api/calendar',payload(j,p,kind='unavailable',job_id=None,title='No team',whole_team=True,resource_ids=[],start_date='2026-11-03',end_date='2026-11-03')).status_code==409


def test_uk_clock_gap_and_ambiguous_fold_choices(app):
    c=client(app);j=job(c);p=person(c)
    gap=payload(j,p,start_date='2026-03-29',end_date='2026-03-29',start_time='01:30',end_time='03:00')
    assert write(c,'/api/calendar',gap).status_code==422
    repeated=payload(j,p,start_date='2026-10-25',end_date='2026-10-25',start_time='01:10',end_time='01:40')
    assert write(c,'/api/calendar',repeated).status_code==422
    first=write(c,'/api/calendar',{**repeated,'start_fold':0,'end_fold':0})
    second=write(c,'/api/calendar',{**repeated,'start_fold':1,'end_fold':1})
    assert first.status_code==second.status_code==200
    assert first.json()['start_utc']=='2026-10-25T00:10:00+00:00'
    assert second.json()['start_utc']=='2026-10-25T01:10:00+00:00'
    assert write(c,'/api/calendar',{**repeated,'start_fold':1,'end_fold':0}).status_code==422


@pytest.mark.parametrize('day,hours',[('2026-03-29',23),('2026-10-25',25)])
def test_all_day_tracks_london_midnight_and_clock_change(app,day,hours):
    c=client(app);j=job(c);p=person(c)
    b=booking(c,j,p,start_date=day,end_date=day,all_day=True,start_time='',end_time='')
    assert (datetime.fromisoformat(b['end_utc'])-datetime.fromisoformat(b['start_utc'])).total_seconds()==hours*3600
    assert len(listing(c,start_date=day,end_date=day).json()['items'])==1
    next_day=(datetime.fromisoformat(day)+timedelta(days=1)).date().isoformat()
    assert listing(c,start_date=next_day,end_date=next_day).json()['items']==[]


def test_versioned_edit_archive_restore_conflicts_and_history(app):
    c=client(app);j=job(c);p=person(c);b=booking(c,j,p)
    r=write(c,'/api/calendar/'+b['id'],edit_payload(b,start_time='10:00',end_time='12:00'),method='put')
    assert r.status_code==200 and r.json()['version']==2
    assert write(c,'/api/calendar/'+b['id'],edit_payload(b),method='put').status_code==409
    assert write(c,'/api/calendar/'+b['id']+'/archive',{'version':2}).status_code==200
    replacement=booking(c,j,p)
    assert write(c,'/api/calendar/'+b['id']+'/restore',{'version':3}).status_code==409
    write(c,'/api/calendar/'+replacement['id']+'/archive',{'version':1})
    assert write(c,'/api/calendar/'+b['id']+'/restore',{'version':3}).status_code==200
    history=c.get('/api/calendar/'+b['id']+'/history').json()
    assert [h['action'] for h in history]==['restore','archive','edited','created']
    assert history[-1]['after_value']['start_time']=='09:00'
    assert len(listing(c,archived=True).json()['items'])==1


def test_retry_and_simultaneous_overlap_have_one_result(app):
    c=client(app);j=job(c);p=person(c);key=str(uuid.uuid4());data=payload(j,p)
    r=write(c,'/api/calendar',data,key=key)
    assert r.status_code==200 and write(c,'/api/calendar',data,key=key).json()==r.json()
    write(c,'/api/calendar/'+r.json()['id']+'/archive',{'version':1})
    clients=[client(app),client(app)]
    with ThreadPoolExecutor(2) as pool:
        results=list(pool.map(lambda cc:write(cc,'/api/calendar',data).status_code,clients))
    assert sorted(results)==[200,409]


def test_team_archive_requires_future_bookings_resolved_and_restore_checks_team(app):
    c=client(app);j=job(c);p=person(c);day=(today_london()+timedelta(days=1)).isoformat()
    b=booking(c,j,p,start_date=day,end_date=day)
    assert write(c,'/api/team/'+p['id']+'/archive',{'version':1}).status_code==409
    write(c,'/api/calendar/'+b['id']+'/archive',{'version':1})
    assert write(c,'/api/team/'+p['id']+'/archive',{'version':1}).status_code==200
    assert write(c,'/api/calendar/'+b['id']+'/restore',{'version':2}).status_code==409
    write(c,'/api/team/'+p['id']+'/restore',{'version':2})
    assert write(c,'/api/calendar/'+b['id']+'/restore',{'version':2}).status_code==200


def test_scheduling_business_portal_csrf_and_validation_boundaries(app):
    c=client(app);b=client(app,1);j=job(c);jb=job(b);p=person(c);pb=person(b);event=booking(c,j,p);pc,_=portal(app,c,j)
    for denied in (pc,TestClient(app,headers=ORIGIN)):
        assert listing(denied).status_code==401
        assert denied.get('/api/calendar/'+event['id']+'/history').status_code==401
        assert write(denied,'/api/calendar',payload(j,p)).status_code==401
    assert listing(b).json()['items']==[]
    for query in ({'resource_id':p['id']},{'job_id':j['id']}):assert listing(b,**query).status_code==404
    assert write(b,'/api/calendar',payload(j,pb)).status_code==404
    assert write(b,'/api/calendar',payload(jb,p)).status_code==404
    assert write(b,'/api/calendar/'+event['id'],edit_payload(event),method='put').status_code==404
    assert write(b,'/api/calendar/'+event['id']+'/archive',{'version':1}).status_code==404
    for change in ({'resource_ids':[]},{'resource_ids':[p['id'],p['id']]},{'whole_team':True},{'business_id':'harbour'},{'start_date':'2026-02-30'},{'end_date':'2026-11-01'},{'start_fold':True},{'all_day':1},{'end_date':'2027-01-01'},{'start_time':'24:00'}):
        assert write(c,'/api/calendar',payload(j,p,**change)).status_code==422,change
    assert 'Private invented booking note' not in pc.get('/api/portal/oak-and-vale/me').text
    c.headers['x-csrf-token']=''
    assert write(c,'/api/calendar',payload(j,p)).status_code==403


def test_schedule_preserves_financials_job_status_and_survives_restart(app):
    c=client(app);j=job(c);p=person(c);q=quote(c,j);i=invoice(c,q)
    before=c.get('/api/workspace').json();saved=booking(c,j,p)
    assert c.get('/api/workspace').json()==before
    assert c.get('/api/documents/invoice/'+i['id']).status_code==200
    restarted=client(create_app(app.state.store.root))
    assert listing(restarted).json()['items'][0]['id']==saved['id']
    from test_customers import contact
    bare=job(c);booking(c,bare,p,start_time='12:00',end_time='13:00');target=contact(c)
    assert write(c,'/api/jobs/'+bare['id']+'/customer-link-preview',{'job_version':1,'customer_id':target['id'],'customer_version':1}).status_code==410
