"""HTTP integration tests. Run ONLY against a new disposable empty database.
python tests/integration.py --base http://127.0.0.1:8087 --token YOUR_SETUP_TOKEN
No third-party Python packages required. Creates fictional test accounts and records.
"""
import argparse, http.cookiejar, urllib.request, urllib.parse, urllib.error, re, json, datetime
P=argparse.ArgumentParser();P.add_argument('--base',required=True);P.add_argument('--token',required=True);args=P.parse_args();BASE=args.base.rstrip('/')
checks=[]
def check(name, condition):
    if not condition: raise AssertionError(name)
    checks.append(name);print('PASS',name,flush=True)
class Client:
    def __init__(self):self.opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
    def request(self,path,data=None):
        request=urllib.request.Request(BASE+'/'+path,data=urllib.parse.urlencode(data,doseq=True).encode() if data is not None else None)
        try:r=self.opener.open(request)
        except urllib.error.HTTPError as e:r=e
        return r.status,r.read().decode('utf-8-sig'),r.url
    def get(self,path):return self.request(path)
    def post(self,path,data,csrf=True):
        if csrf:
            status,html,url=self.get(path);m=re.search(r'name="csrf" value="([^"]+)"',html)
            if not m:raise AssertionError('Missing CSRF on '+path+' '+html[:200])
            data=dict(data,csrf=m.group(1))
        return self.request(path,data)
    def login(self,email,password):return self.post('login.php',{'email':email,'password':password})
a=Client();check('Anonymous admin page requires sign in','login.php' in a.get('index.php')[2])
r=a.post('install.php',{'setup_token':args.token,'name':'Haven Admin','email':'admin@example.test','password':'Local-test-password-2468','demo':'1'})
check('Installer creates schema and demo records',r[0]==200 and 'login.php' in r[2])
check('Installer locks after installation',a.get('install.php')[0]==403)
check('Administrator sign in', 'index.php' in a.login('admin@example.test','Local-test-password-2468')[2])
modules=['properties','units','tenants','maintainers','agreements','invoices','transactions','maintenance','tickets','contacts','inquiries','users']
for m in modules:
    for action in ['list','create','view&id=1']:
        r=a.get('index.php?page='+m+'&action='+action)
        check(m+' '+action+' renders',r[0]==200 and 'Something went wrong' not in r[1])
for m in ['dashboard','finance','roles','settings','account']:
    r=a.get('index.php?page='+m);check(m+' renders',r[0]==200 and 'Something went wrong' not in r[1])
check('CSRF forgery blocked',a.post('index.php?page=contacts',{'operation':'save','csrf':'bad'},False)[0]==419)

def save(m,data,id=None):
    path='index.php?page='+m+'&action='+('edit&id='+str(id) if id else 'create')
    return a.post(path,dict(data,operation='save'))
def saved_id(r):
    check('Save redirects to record view','action=view' in r[2]);return int(urllib.parse.parse_qs(urllib.parse.urlparse(r[2]).query)['id'][0])
contact=saved_id(save('contacts',{'name':'<script>alert(1)</script>','email':'check@example.test','phone':'123','type':'vendor','company':'=1+1','notes':'Test contact'}))
r=a.get(f'index.php?page=contacts&action=view&id={contact}')
check('Stored HTML is escaped','<script>alert(1)</script>' not in r[1] and '&lt;script&gt;' in r[1])
check('CSV formula values neutralized',"'=1+1" in a.get('index.php?page=contacts&action=export')[1])
r=save('contacts',{'name':'Updated vendor','type':'vendor'},contact);check('CRUD update works','Updated vendor' in r[1])
r=a.post(f'index.php?page=contacts&action=view&id={contact}',{'operation':'delete'});check('CRUD delete works','Record deleted' in r[1])

r=save('agreements',{'tenant_id':2,'unit_id':1,'start_date':'2026-01-01','end_date':'2027-01-01','rent':'450','deposit':'450','status':'active','terms':'Test duplicate'})
check('Duplicate active occupancy is blocked','already has an active agreement' in r[1])
r=save('units',{'property_id':1,'name':'A-101','bedrooms':2,'bathrooms':2,'area':98,'rent':450,'status':'maintenance'},1)
check('Occupied unit cannot be taken out of service','End the active agreement' in r[1])
r=save('agreements',{'tenant_id':2,'unit_id':3,'start_date':'2027-01-01','end_date':'2026-01-01','rent':'450','deposit':'450','status':'draft','terms':'Invalid dates'})
check('Invalid agreement dates are blocked','End date must be on or after' in r[1])
# Current-month invoice 22 is unpaid in the installer demo.
tx={'type':'income','category':'Rent','amount':'421','transaction_date':str(datetime.date.today()),'method':'Cash','invoice_id':22,'description':'test'}
r=save('transactions',tx);check('Overpayment is blocked','exceeds the remaining invoice balance' in r[1])
tx['amount']='120';payment=saved_id(save('transactions',tx))
r=a.get('index.php?page=invoices&action=view&id=22');check('Partial payment changes remaining balance','USD 300.00' in r[1])
r=a.post('index.php?page=invoices&action=view&id=22',{'operation':'delete'});check('Paid invoice cannot be voided','Void the invoice payments' in r[1])
r=a.post(f'index.php?page=transactions&action=view&id={payment}',{'operation':'delete'});check('Payment void succeeds','Record voided' in r[1])
check('Voided payment restores invoice balance','USD 420.00' in a.get('index.php?page=invoices&action=view&id=22')[1])
r=save('transactions',dict(tx,amount='-12'));check('Negative transaction rejected','nonnegative number' in r[1])
r=save('transactions',dict(tx,amount='10'),payment);check('Posted financial history is immutable','cannot be edited' in r[1])
# Create a bill in a valid future month, void, and replace it.
next_month=(datetime.date.today().replace(day=1)+datetime.timedelta(days=40)).replace(day=1)
bill={'agreement_id':1,'period':str(next_month)[:7],'due_date':str(next_month),'amount':'450'}
inv=saved_id(save('invoices',bill));r=save('invoices',bill);check('Duplicate monthly invoice is blocked','already exists' in r[1])
a.post(f'index.php?page=invoices&action=view&id={inv}',{'operation':'delete'})
replacement=saved_id(save('invoices',bill));check('Voided invoice can be replaced',replacement!=inv)

# Create and link scoped accounts.
u1=saved_id(save('users',{'name':'Tenant Tester','email':'tenant@example.test','role_id':3,'password':'Tenant-test-password-123','active':1}))
u2=saved_id(save('users',{'name':'Maintainer Tester','email':'maintainer@example.test','role_id':4,'password':'Maintainer-test-pass-123','active':1}))
save('tenants',{'name':'Amina Hassan','email':'amina@example.test','phone':'123456','user_id':u1},1)
save('maintainers',{'name':'Abdi Maintenance','email':'maintenance@example.test','phone':'123456','specialty':'Plumbing','user_id':u2,'status':'available'},1)
tenant=Client();check('Tenant sign in','index.php' in tenant.login('tenant@example.test','Tenant-test-password-123')[2])
check('Tenant cannot open another agreement',tenant.get('index.php?page=agreements&action=view&id=2')[0]==404)
check('Tenant cannot open another invoice',tenant.get('index.php?page=invoices&action=view&id=22')[0]==404)
check('Tenant cannot open user management',tenant.get('index.php?page=users')[0]==403)
check('Tenant cannot access finance report',tenant.get('index.php?page=finance')[0]==403)
r=tenant.post('index.php?page=maintenance&action=create',{'operation':'save','unit_id':1,'title':'Tenant request','description':'Please inspect this issue.','priority':'normal'});check('Tenant can report their own unit','action=view' in r[2])
r=tenant.post('index.php?page=maintenance&action=create',{'operation':'save','unit_id':5,'title':'Illegal other unit','description':'Attempt','priority':'normal'});check('Tenant cannot report another unit','Choose a valid Unit' in r[1])
r=tenant.post('index.php?page=tickets&action=create',{'operation':'save','subject':'Tenant question','message':'Question for management','priority':'normal'});ticket=int(urllib.parse.parse_qs(urllib.parse.urlparse(r[2]).query)['id'][0]);check('Tenant support ticket creation','action=view' in r[2])
a.post(f'index.php?page=tickets&action=view&id={ticket}',{'operation':'reply','message':'We will assist you.'});check('Tenant sees support reply','We will assist you.' in tenant.get(f'index.php?page=tickets&action=view&id={ticket}')[1])
maint=Client();maint.login('maintainer@example.test','Maintainer-test-pass-123')
check('Maintainer cannot read another assignment',maint.get('index.php?page=maintenance&action=view&id=3')[0]==404)
r=maint.post('index.php?page=maintenance&action=edit&id=1',{'operation':'save','status':'resolved','unit_id':5,'estimated_cost':9000})
check('Maintainer can resolve own assignment','Resolved' in r[1]);check('Maintainer injected fields ignored','USD 35.00' in r[1] and 'USD 9,000.00' not in r[1])
# Staff permissions are enforced server-side.
a.post('index.php?page=roles',{'name':'Read only','permissions[]':['properties.view']})
u3=saved_id(save('users',{'name':'Reader','email':'reader@example.test','role_id':5,'password':'Read-only-password-123','active':1}))
reader=Client();reader.login('reader@example.test','Read-only-password-123')
check('View-only staff can list properties',reader.get('index.php?page=properties')[0]==200)
check('View-only staff cannot create properties',reader.get('index.php?page=properties&action=create')[0]==403)
check('Staff cannot access settings',reader.get('index.php?page=settings')[0]==403)
check('Last administrator is protected','cannot disable yourself' in save('users',{'name':'Admin','email':'admin@example.test','role_id':1},1)[1])
# Public web form is persisted and accessible only to staff.
guest=Client();r=guest.get('website.php');check('Public property listing renders','Acacia Residences' in r[1])
r=guest.post('website.php?property=1',{'name':'Public Test','email':'public@example.test','phone':'123','message':'Please arrange a viewing next week.'});check('Public inquiry accepted','Your inquiry has been sent' in r[1])
check('Public inquiry appears in admin','Public Test' in a.get('index.php?page=inquiries')[1])
r=a.post('index.php?page=settings&tab=payment',{'payment_methods':'Cash,Bank transfer,Mobile money','bank_name':'Demo Bank','bank_account':'TEST-ONLY','mobile_account':'Demo phone','payment_instructions':'Use invoice reference with payment.'});check('Payment settings save','Settings saved' in r[1]);check('Tenant invoice shows payment instructions','Use invoice reference with payment.' in tenant.get('index.php?page=invoices&action=view&id=21')[1])
r=a.get('index.php?page=invoices&q=Amina');check('Invoice search resolves tenant names','AGR-1' in r[1] and 'AGR-2' not in r[1])
save('users',{'name':'Reader','email':'reader@example.test','role_id':5,'password':'New-reader-password-456','active':1},u3)
check('Password reset invalidates existing login','login.php' in reader.get('index.php')[2])
check('Unknown route returns 404',a.get('index.php?page=arbitrary_table')[0]==404)
check('GET logout is refused',a.get('logout.php')[0]==405)
print(json.dumps({'passed':len(checks),'failed':0},indent=2))
