"""Alert engine and HTTP permissions in an isolated database; no pilot data touched."""
import os, subprocess, tempfile, socket, time, uuid, json, urllib.request, urllib.error, http.cookiejar
from pathlib import Path
root=Path(__file__).resolve().parents[2]
state=Path.home()/'.local/share/homecore-dev'
name='homecore_alert_test_'+uuid.uuid4().hex
mysql=['/opt/bitnami/mariadb/bin/mariadb','--no-defaults','--socket='+str(state/'mysql.sock'),'-uroot','-N']
def sql(q): return subprocess.check_output(mysql+['-e',q],text=True).strip()
def q(s): return sql('USE '+name+'; '+s)
sql('CREATE DATABASE '+name)
server=None
with tempfile.TemporaryDirectory() as sessions:
 env={**os.environ,'HOMECORE_ENV':'development','HOMECORE_SESSION_PATH':sessions,'HOMECORE_DB_DSN':f'mysql:unix_socket={state}/mysql.sock;dbname={name};charset=utf8mb4','HOMECORE_DB_USER':'root','HOMECORE_DB_PASSWORD':''}
 try:
  subprocess.run(['php',str(root/'backend/bin/migrate.php')],env=env,check=True)
  with socket.socket() as s: s.bind(('127.0.0.1',0)); port=s.getsockname()[1]
  with tempfile.TemporaryFile() as log:
   server=subprocess.Popen(['php','-S',f'127.0.0.1:{port}',str(root/'backend/public/index.php')],env=env,stdout=log,stderr=log)
   for _ in range(100):
    try:
     with socket.create_connection(('127.0.0.1',port),.1): break
    except OSError: time.sleep(.05)
   class Client:
    def __init__(self): self.opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())); self.csrf=''
    def req(self,path,method='GET',data=None,status=200,csrf=True):
     r=urllib.request.Request(f'http://127.0.0.1:{port}/api/v1'+path,method=method,data=json.dumps(data).encode() if data is not None else None,headers={'Content-Type':'application/json','X-CSRF-Token':self.csrf if csrf else ''})
     try: res=self.opener.open(r)
     except urllib.error.HTTPError as e: res=e
     b=json.loads(res.read());assert res.code==status,(path,res.code,b)
     if 'csrf_token' in b:self.csrf=b['csrf_token']
     return b
    def reg(self,user):
     self.req('/auth/csrf');return self.req('/auth/register','POST',{'name':user,'username':user,'password':'test-password-123'},201)['user']['id']
   owner,other,resident,child=Client(),Client(),Client(),Client()
   owner.reg('owner');other.reg('other');resident.reg('resident')
   home=owner.req('/homes','POST',{'name':'Test','timezone':'UTC'},201)['home']['id']
   room=owner.req(f'/homes/{home}/rooms','POST',{'name':'Room'},201)['room']['id']
   room2=owner.req(f'/homes/{home}/rooms','POST',{'name':'Private'},201)['room']['id']
   device=owner.req(f'/homes/{home}/devices','POST',{'name':'Sensor','room_id':room},201)['device']['id']
   private=owner.req(f'/homes/{home}/devices','POST',{'name':'Private','room_id':room2},201)['device']['id']
   path=f'/devices/{device}/alert-rules'
   rule={'metric':'temperature','enabled':True,'minimum':10,'maximum':30,'duration_seconds':60}
   owner.req(path,'POST',rule,403,False)
   other.req(path,status=404);other.req(f'/homes/{home}/alerts',status=404)
   owner.req(f'/homes/{home}/members','POST',{'identifier':'resident','role':'resident'},201)
   resident.req(path,'POST',rule,403)
   owner.req(f'/homes/{home}/children','POST',{'name':'Child','username':'child','password':'test-password-123','enabled':True,'room_ids':[room]},201)
   child.req('/auth/csrf');child.req('/auth/login','POST',{'identifier':'child','password':'test-password-123'})
   child.req(path,'POST',rule,403); child.req(f'/devices/{private}/alert-rules',status=404)
   for bad in [{'duration_seconds':0},{'minimum':31},{'maximum':'bad'},{'enabled':1},{'metric':'invalid'}]:owner.req(path,'POST',{**rule,**bad},422)
   owner.req(path,'POST',rule)
   rid=int(q(f'SELECT id FROM alert_rules WHERE device_id={device}'))
   def engine():subprocess.run(['php',str(root/'backend/bin/evaluate-alerts.php')],env=env,check=True)
   def sample(temp,seconds,source='sensor',dev=device):
    q(f"INSERT INTO sensor_readings (device_id,message_id,temperature,humidity,source,received_at) VALUES ({dev},'{uuid.uuid4().hex}',{temp},71,'{source}',UTC_TIMESTAMP(3)-INTERVAL {seconds} SECOND)")
   def count():return int(q('SELECT COUNT(*) FROM alert_events'))
   # Ignore simulated values; a normal reading resets the dwell timer.
   sample(40,240,'simulated');engine();assert count()==0
   sample(40,200);sample(25,180);sample(40,160);sample(40,120);engine();assert count()==0
   sample(40,100);engine();assert count()==1
   engine();sample(41,80);engine();assert count()==1
   sample(25,60);engine();assert q('SELECT reason FROM alert_events')=='normal'
   # A >90s gap cannot be counted as sustained abnormal readings.
   sample(40,40);engine();sample(40,-100);engine();assert count()==1
   # Reset configuration and begin a distinct episode.
   owner.req(path,'POST',rule);sample(40,80);sample(40,20);engine();assert count()==2
   owner.req(path,'POST',{**rule,'enabled':False});assert q('SELECT reason FROM alert_events ORDER BY id DESC LIMIT 1')=='configuration_changed'
   # Missing real telemetry even with recent simulated telemetry.
   owner.req(path,'POST',{'metric':'missing','enabled':True,'duration_seconds':60})
   q(f"UPDATE sensor_readings SET received_at=UTC_TIMESTAMP(3)-INTERVAL 200 SECOND WHERE device_id={device}")
   q(f"UPDATE alert_rules SET configured_at=UTC_TIMESTAMP(3)-INTERVAL 120 SECOND WHERE metric='missing'")
   sample(40,0,'simulated');engine();assert count()==3
   engine();assert count()==3
   sample(24,0);engine();assert q('SELECT reason FROM alert_events ORDER BY id DESC LIMIT 1')=='normal'
   # Paused devices close an active event, and rules do not alert while paused.
   q("UPDATE alert_rules SET configured_at=UTC_TIMESTAMP(3)-INTERVAL 120 SECOND WHERE metric='missing'")
   q(f"UPDATE sensor_readings SET received_at=UTC_TIMESTAMP(3)-INTERVAL 200 SECOND WHERE device_id={device}")
   engine();assert count()==4
   owner.req(f'/devices/{device}','PATCH',{'enabled':False});engine();assert q('SELECT reason FROM alert_events ORDER BY id DESC LIMIT 1')=='paused'
   engine();assert count()==4
   # A hidden room's event must not appear for child accounts.
   owner.req(f'/devices/{private}/alert-rules','POST',{'metric':'missing','enabled':True,'duration_seconds':60})
   q(f"UPDATE alert_rules SET configured_at=UTC_TIMESTAMP(3)-INTERVAL 120 SECOND WHERE device_id={private}")
   engine();assert count()==5
   assert len(owner.req(f'/homes/{home}/alerts')['alerts'])==5
   assert len(child.req(f'/homes/{home}/alerts')['alerts'])==4
   assert len(resident.req(f'/homes/{home}/alerts')['alerts'])==5
   # Humidity lower bound, exact-bound recovery, and upper bound.
   owner.req(f'/devices/{private}/alert-rules','POST',{'metric':'humidity','enabled':True,'minimum':30,'maximum':80,'duration_seconds':60})
   def humid(value,age):
    q(f"INSERT INTO sensor_readings (device_id,message_id,temperature,humidity,source,received_at) VALUES ({private},'{uuid.uuid4().hex}',25,{value},'sensor',UTC_TIMESTAMP(3)-INTERVAL {age} SECOND)")
   humid(20,80);humid(20,20);engine()
   assert q("SELECT observed_value FROM alert_events WHERE metric='humidity'")=='20.00'
   humid(30,10);engine();assert q("SELECT reason FROM alert_events WHERE metric='humidity'")=='normal'
   humid(90,80);humid(90,20);engine()
   assert q("SELECT COUNT(*) FROM alert_events WHERE metric='humidity'")=='2'
   if os.environ.get('HOMECORE_UI_TEST')=='1':
    subprocess.run(['/home/bitnami/.local/share/homecore-tools/node-v24.21.0-linux-x64/bin/node',str(root/'websocket-server/tests/alerts-ui.mjs')],env={**env,'HOMECORE_TEST_URL':f'http://127.0.0.1:{port}/'},check=True)
   print('PASS: dwell, normal recovery, gaps, simulation exclusion, deduplication, restart, missing readings, pause, validation, CSRF and home/child permissions')
 finally:
  if server: server.terminate();server.wait()
  sql('DROP DATABASE '+name)
