"""Ten dated public product-page observations, not purchases or lab tests."""
from decimal import Decimal, ROUND_HALF_UP
import copy
import json

CHECKED='2026-10-24'
PREFIX='https://www.tesco.com/shop/en-GB/products/'
RECORDS=[
    {'id':'rice_1kg','name':'Tesco Basmati Rice 1Kg','url':PREFIX+'254877477','price_gbp':'1.79','quantity':'1','unit':'kg','state':'dry','family':'basmati_rice','display_rate':'1.79','display_basis':'kg','stock_note':'location_availability_not_verified'},
    {'id':'rice_finest_500g','name':'Tesco Finest Basmati Rice 500G','url':PREFIX+'266953910','price_gbp':'1.65','quantity':'500','unit':'g','state':'dry','family':'basmati_rice','display_rate':'3.30','display_basis':'kg','stock_note':'page_said_out_of_stock','limit':'Different product tier; unit arithmetic does not establish equal quality.'},
    {'id':'rice_cooked_250g','name':'Tesco Microwave Basmati Rice 250G','url':PREFIX+'251680648?gQT=1','price_gbp':'0.70','quantity':'250','unit':'g','state':'cooked','family':'basmati_rice','display_rate':'2.80','display_basis':'kg','stock_note':'location_availability_not_verified','limit':'Description says cooked rice; ingredients include water. No dry-to-cooked yield measured.'},
    {'id':'tuna_145g','name':'Tesco Tuna Chunks In Spring Water 145G','url':PREFIX+'303887546','price_gbp':'0.65','net_g':145,'drained_g':102,'quantity':'102','unit':'g','state':'drained','family':'tuna_spring_water','display_rate':'6.37','display_basis':'kg_drained','stock_note':'location_availability_not_verified'},
    {'id':'tuna_4x145g','name':'Tesco Tuna Chunks In Spring Water 4 X 145G','url':PREFIX+'303882239','price_gbp':'2.49','net_g':580,'net_description':'4 x 145g','drained_g':408,'drained_description':'4 x 102g','quantity':'408','unit':'g','state':'drained','family':'tuna_spring_water','display_rate':'6.10','display_basis':'kg_drained','stock_note':'location_availability_not_verified'},
    {'id':'eggs_6','name':'Tesco Large Free Range Eggs 6 Pack','url':PREFIX+'250802567','price_gbp':'2.10','quantity':'6','unit':'each','state':'counted','family':'large_free_range_eggs','display_rate':'0.35','display_basis':'each','stock_note':'location_availability_not_verified','pack_mass_g':None},
    {'id':'eggs_12','name':'Tesco Large Free Range Eggs 12 Pack','url':PREFIX+'260298456','price_gbp':'3.35','quantity':'12','unit':'each','state':'counted','family':'large_free_range_eggs','display_rate':'0.28','display_basis':'each','stock_note':'location_availability_not_verified','pack_mass_g':None},
    {'id':'corn_325g','name':'Tesco Naturally Sweet Sweetcorn 325G','url':PREFIX+'254875341?isSub=true','price_gbp':'0.53','net_g':325,'drained_g':260,'quantity':'260','unit':'g','state':'drained','family':'sweetcorn','display_rate':'2.04','display_basis':'kg_drained','stock_note':'location_availability_not_verified'},
    {'id':'corn_frozen_1kg','name':'Tesco Supersweet Sweetcorn 1Kg','url':PREFIX+'259488440','price_gbp':'1.90','quantity':'1','unit':'kg','state':'frozen','family':'sweetcorn','display_rate':'1.90','display_basis':'kg','stock_note':'location_availability_not_verified','limit':'Frozen mass and drained canned mass do not establish equal prepared portions.'},
    {'id':'milk_2l','name':'Tesco Semi-Skimmed Milk 2 Litres','url':PREFIX+'253656619','price_gbp':'1.60','quantity':'2','unit':'L','state':'liquid','family':'semi_skimmed_milk','display_rate':'0.80','display_basis':'litre','stock_note':'page_said_out_of_stock','limit':'Northern Ireland milk described on page. No conversion to mass or local availability claim.'},
]


def positive(value, zero=False):
    if value is None:raise ValueError('Unknown value is not zero')
    d=Decimal(str(value))
    if not d.is_finite() or d < 0 or (not zero and d==0):raise ValueError('Invalid amount')
    return d


def quantity(value,unit):
    factors={'g':('mass_g','1'),'kg':('mass_g','1000'),'ml':('volume_ml','1'),'L':('volume_ml','1000'),'each':('count','1')}
    if unit not in factors:raise ValueError('Unsupported unit; no guessed conversion')
    basis,factor=factors[unit]
    return basis,positive(value)*Decimal(factor)


def rate(row):
    basis,q=quantity(row['quantity'],row['unit'])
    if row.get('drained_g') is not None:
        if positive(row['drained_g'])>positive(row['net_g']):raise ValueError('Drained amount exceeds net')
        if q != positive(row['drained_g']):raise ValueError('Selected amount must match declared drained basis')
    scale=Decimal(1 if basis=='count' else 1000)
    raw=positive(row['price_gbp'],zero=True)/q*scale
    rounded=format(raw.quantize(Decimal('.01'),rounding=ROUND_HALF_UP),'.2f')
    return {'id':row['id'],'normalised_basis':basis,'normalised_quantity':str(q),'rate_before_rounding':str(raw),'rounded_rate':rounded,'per':'each' if basis=='count' else ('litre' if basis=='volume_ml' else 'kg'),'display_matches':rounded==row['display_rate']}


def compare(a,b):
    qa,_=quantity(a['quantity'],a['unit']);qb,_=quantity(b['quantity'],b['unit'])
    if qa!=qb:return {'status':'blocked','reason':'different dimensions; density or mass per item not supplied'}
    if a['family']!=b['family'] or a['state']!=b['state']:return {'status':'blocked','reason':'different family or preparation state; no equivalent-yield model'}
    return {'status':'quantity_basis_comparable','reason':'Same family/state and dimension only; quality, actual portions, availability and waste are not established.'}


def run():
    rows=[rate(r) for r in RECORDS]
    assert len(rows)==10 and all(r['display_matches'] for r in rows)
    by_id={r['id']:r for r in RECORDS}
    gross_tuna=(Decimal('.65')/145*1000).quantize(Decimal('.01'),rounding=ROUND_HALF_UP)
    drained_tuna=rows[3]['rounded_rate']
    assert (gross_tuna,drained_tuna)==(Decimal('4.48'),'6.37')
    assert Decimal('.65')*4-Decimal('2.49')==Decimal('.11')
    assert Decimal('3.35')/12*12==Decimal('3.35')
    assert Decimal('.28')*12==Decimal('3.36')
    assert quantity('2','L')==('volume_ml',Decimal('2000'))
    pairs={name:compare(by_id[a],by_id[b]) for name,a,b in [
        ('tuna_single_and_four','tuna_145g','tuna_4x145g'),('eggs_six_and_twelve','eggs_6','eggs_12'),
        ('different_rice_tiers','rice_1kg','rice_finest_500g'),('dry_and_cooked_rice','rice_1kg','rice_cooked_250g'),
        ('drained_and_frozen_corn','corn_325g','corn_frozen_1kg'),('volume_to_mass_guard','milk_2l','tuna_145g')]}
    assert pairs['dry_and_cooked_rice']['status']==pairs['drained_and_frozen_corn']['status']==pairs['volume_to_mass_guard']['status']=='blocked'
    rejected=[]
    wrong_drained=copy.deepcopy(by_id['tuna_145g']);wrong_drained['drained_g']=200
    tests=[('unknown_unit',lambda:quantity('1','oz')),('zero_quantity',lambda:quantity('0','g')),
           ('unknown_quantity',lambda:quantity(None,'g')),('negative_price',lambda:positive('-1',zero=True)),
           ('nonfinite_price',lambda:positive('NaN')),('drained_exceeds_net',lambda:rate(wrong_drained))]
    for name,call in tests:
        try:call()
        except ValueError:rejected.append({'case':name,'result':'rejected_as_expected'})
        else:raise AssertionError('Invalid input accepted: '+name)
    return {'checked_on':CHECKED,'scope':'Tesco en-GB public pages, displayed GBP prices; not a basket quote, physical label inspection, nutrition or storage experiment. No member-only offers used. Delivery, local stock and actual usage not verified.',
            'observations':RECORDS,'rates':rows,'comparisons':pairs,
            'specific_checks':{'tuna_per_kg_net_denominator':str(gross_tuna),'tuna_per_kg_drained_denominator':drained_tuna,'four_singles_less_multipack_gbp':'0.11','eggs_rounded_label_times_12_gbp':'3.36','eggs_actual_listed_pack_gbp':'3.35','milk_ml':'2000'},'invalid_inputs':rejected}


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