#!/usr/bin/env python3
"""Recette locale de capacité continue, sans Sharetribe ni réservation.
Minutes d'une journée fictive déjà résolue, hors changement d'heure.
Segments disjoints de capacité restante, pas des plages expanded superposées.
"""
from itertools import permutations
import json

def integer(x, name, minimum=0):
    if type(x) is not int or x < minimum:
        raise ValueError(name)

def validate(segments):
    rows = []
    for s in segments:
        if not isinstance(s, (tuple,list)) or len(s)!=3:
            raise ValueError('segment')
        start,end,seats=s
        for x,n in [(start,'début'),(end,'fin'),(seats,'places')]:integer(x,n)
        if start>=end or end>1440:raise ValueError('bornes')
        rows.append((start,end,seats))
    rows.sort()
    if any(a[1]>b[0] for a,b in zip(rows,rows[1:])):
        raise ValueError('segments superposés : normalisation requise')
    return rows

def fits(segments, start, duration, seats, buffer_before=0):
    rows=validate(segments)
    for x,n,minimum in [(start,'départ',0),(duration,'durée',1),
                        (seats,'places demandées',1),(buffer_before,'préparation',0)]:
        integer(x,n,minimum)
    left,right=start-buffer_before,start+duration
    if left<0 or right>1440:raise ValueError('hors journée')
    cursor=left
    for begin,end,capacity in rows:
        if end<=cursor:continue
        if begin>cursor:return False
        if capacity<seats:return False
        cursor=min(end,right)
        if cursor==right:return True
    return False

def departures(segments, first=555, last=645, duration=90,seats=2,step=15,buffer_before=0):
    for x,n,minimum in [(first,'premier',0),(last,'dernier',0),(step,'pas',1)]:integer(x,n,minimum)
    if first>last:raise ValueError('ordre départs')
    return [t for t in range(first,last+1,step) if fits(segments,t,duration,seats,buffer_before)]

def hhmm(m):return f'{m//60:02d}:{m%60:02d}'

def run():
    checks=0
    def eq(a,b):
        nonlocal checks
        assert a==b,(a,b)
        checks+=1
    def refuses(fn):
        nonlocal checks
        try:fn()
        except ValueError:checks+=1
        else:raise AssertionError('refus attendu')
    # 09:15–10:00 :2 ;10:00–10:30 :1 ;10:30–12:15 :2.
    segments=[(555,600,2),(600,630,1),(630,735,2)]
    eq(departures(segments),[630,645])
    eq(departures(segments,seats=1),[555,570,585,600,615,630,645])
    eq(departures(segments,seats=3),[])
    for perm in permutations(segments):eq(departures(perm),[630,645])
    eq(fits(segments,555,45,2),True)  # fin exclusive à10:00
    eq(fits(segments,600,30,2),False)
    eq(fits(segments,630,90,2),True)
    eq(fits(segments,645,90,2),True)
    eq(fits(segments,660,90,2),False) # dépasse fermeture
    eq(departures(segments,buffer_before=10),[645])
    eq(fits(segments,630,90,2,10),False)
    eq(fits(segments,645,90,2,10),True)
    # Plages adjacentes : pas besoin d'un segment unique couvrant90min.
    adjacent=[(555,600,2),(600,630,2),(630,735,2)]
    eq(fits(adjacent,555,90,2),True)
    eq(departures(adjacent),[555,570,585,600,615,630,645])
    for perm in permutations(adjacent):eq(fits(perm,555,90,2),True)
    # Trou réellement non disponible, même pouruneplace.
    gap=[(555,600,2),(630,735,2)]
    eq(fits(gap,555,90,1),False)
    eq(departures(gap,seats=1),[630,645])
    eq(fits([],555,90,1),False)
    zero=[(555,600,2),(600,630,0),(630,735,2)]
    eq(departures(zero,seats=1),[630,645])
    # Deux observations successives, pas une transaction atomique simulée.
    observed_free=[(555,735,2)]
    eq(fits(observed_free,555,90,2),True)
    eq(fits(segments,555,90,2),False)
    # Vérifier seulement le départ retient3fauxpositifs.
    naive=[t for t in range(555,646,15) if any(a<=t<b and c>=2 for a,b,c in segments)]
    eq(naive,[555,570,585,630,645])
    eq(sorted(set(naive)-set(departures(segments))),[555,570,585])
    eq(sum(b-a for a,b,c in segments if c>=2),150)
    eq(fits(segments,555,90,2),False) # 150minutesdispersées≠90continues
    # Une identification éphémère ne change pas l'intervalle analysé.
    api_rows=[{'id':f'old{i}','start':a,'end':b,'seats':c} for i,(a,b,c) in enumerate(segments)]
    renewed=[{**r,'id':f'new{i}'} for i,r in enumerate(api_rows)]
    eq(departures([(r['start'],r['end'],r['seats']) for r in renewed]),[630,645])
    # Notre format refuse d'additionnerdesintervallesexpandedqui sechevauchent.
    refuses(lambda:fits([(555,735,1),(555,600,2)],555,90,2))
    for bad in [[(600,555,2)],[(555,555,2)],[(555,735,-1)],[(555,1500,2)],[(555,735,True)],[(555,735)]]:
        refuses(lambda rows=bad:fits(rows,555,90,2))
    for kwargs in [dict(start=True),dict(duration=0),dict(duration=1.5),dict(seats=0),dict(seats=True),dict(buffer_before=-1),dict(start=5,buffer_before=10)]:
        args=dict(start=555,duration=90,seats=2);args.update(kwargs)
        refuses(lambda kw=args:fits(segments,**kw))
    refuses(lambda:departures(segments,step=0))
    eq(90*2,180)
    eq((90//15)*90*2,1080)
    eq(1080-180,900)
    eq(12*8,96)
    return dict(status='PASS',checks=checks,network=False,booking=False,
                one_seat=[hhmm(t) for t in departures(segments,seats=1)],
                two_seats=[hhmm(t) for t in departures(segments)],
                two_with_preparation=[hhmm(t) for t in departures(segments,buffer_before=10)],
                invalid_start_only=[hhmm(t) for t in sorted(set(naive)-set(departures(segments)))],
                fixed_price_example=180,wrong_quantum_price=1080,support_minutes_hypothesis=96)

if __name__=='__main__':print(json.dumps(run(),ensure_ascii=False,indent=2))
