#!/usr/bin/env python3
"""Fiction locale : contributions d'un export GSC. Aucun compte/API/BigQuery utilisé.
Python 3, bibliothèque standard. Les valeurs ne décrivent aucun site réel.
"""
from dataclasses import dataclass, replace
from fractions import Fraction
import json

@dataclass(frozen=True)
class Row:
    url: str
    query: str | None
    anonymous: bool
    impressions: int
    clicks: int
    sum_position: int

# Toutes les lignes : 20 octobre 2026, WEB, MOBILE, fra, propriété fictive.
# Deux contributions identiques sont intentionnellement conservées.
ROWS = [
    Row('/integration-api', 'connecteur erp', False, 400, 4, 1600),
    Row('/integration-api', 'connecteur erp', False, 400, 4, 1600),
    Row('/integration-api', 'connecteur erp', False, 200, 12, 200),
    Row('/integration-api', None, True, 200, 8, 600),
    Row('/guide-connecteur', 'connecteur erp', False, 100, 10, 100),
]

def total(rows):
    impressions = sum(r.impressions for r in rows)
    clicks = sum(r.clicks for r in rows)
    positions = sum(r.sum_position for r in rows)
    return {
        'impressions': impressions, 'clicks': clicks, 'sum_position': positions,
        'ctr': Fraction(clicks, impressions) if impressions else None,
        'position': Fraction(positions, impressions) + 1 if impressions else None,
    }

def missing_days(expected, successful):
    # successful représente seulement des dates présentes dans ExportLog.
    return sorted(set(expected) - set(successful))

def replace_partition(partitions, date, rows):
    # Modèle d'une table dérivée locale ; ne modifie jamais une table Google.
    partitions[date] = list(rows)

checks = []
def check(name, condition):
    if not condition:
        raise AssertionError(name)
    checks.append(name)

visible_a = [r for r in ROWS if r.url == '/integration-api' and not r.anonymous]
a = [r for r in ROWS if r.url == '/integration-api']
visible = [r for r in ROWS if not r.anonymous]
unknown = [r for r in ROWS if r.anonymous]
check('deux contributions strictement identiques existent', ROWS[0] == ROWS[1])
check('A visible : 1000 impressions et20clics', (total(visible_a)['impressions'], total(visible_a)['clicks']) == (1000, 20))
check('CTR pondéré A visible :2pourcent', total(visible_a)['ctr'] == Fraction(2, 100))
check('position A visible :4.4', total(visible_a)['position'] == Fraction(22, 5))
check('A complet :1200impressions28clics', (total(a)['impressions'], total(a)['clicks']) == (1200, 28))
check('URL complètes :1300impressions38clics', (total(ROWS)['impressions'], total(ROWS)['clicks']) == (1300, 38))
check('visible :1100impressions30clics', (total(visible)['impressions'], total(visible)['clicks']) == (1100, 30))
check('anonyme :200impressions8clics', (total(unknown)['impressions'], total(unknown)['clicks']) == (200, 8))
check('anonymat NULL ou vide même total', total([replace(r, query='') if r.anonymous else r for r in ROWS]) == total(ROWS))
check('aucune impression :ratio absent', total([])['ctr'] is None and total([])['position'] is None)
# Témoins volontairement incorrects : ils doivent diverger du contrat.
distinct = total(list(set(visible_a)))
check('témoin DISTINCT perd400impressions4clics', (distinct['impressions'], distinct['clicks']) == (600, 16) and distinct != total(visible_a))
mean_ctr = sum(Fraction(r.clicks, r.impressions) for r in visible_a) / len(visible_a)
check('témoin moyenne des CTR :2.6667pourcent au lieu de2', mean_ctr == Fraction(2, 75) and mean_ctr != total(visible_a)['ctr'])
mean_position = sum(Fraction(r.sum_position, r.impressions) + 1 for r in visible_a) / len(visible_a)
check('témoin moyenne des positions :4 au lieu de4.4', mean_position == 4 and mean_position != total(visible_a)['position'])
check('témoin filtre query détruit le total complet', total(visible)['clicks'] != total(ROWS)['clicks'])
expected = ['2026-10-20', '2026-10-21', '2026-10-22']
site_days = expected[:]
url_days = expected[:2]
check('site récent ne prouve pas URL récente', missing_days(expected, site_days) == [] and missing_days(expected, url_days) == ['2026-10-22'])
check('max date ne détecte pas un trou intérieur', max([expected[0], expected[2]]) == max(expected) and missing_days(expected, [expected[0], expected[2]]) == [expected[1]])
partitions = {'2026-10-20': list(ROWS)}
# Révision fictive :12devient14clics, impressions inchangées.
corrected = [replace(r, clicks=14) if i == 2 else r for i, r in enumerate(ROWS)]
replace_partition(partitions, '2026-10-20', corrected)
check('révision remplace la partition :1300impressions40clics', (total(partitions['2026-10-20'])['impressions'], total(partitions['2026-10-20'])['clicks']) == (1300, 40))
replace_partition(partitions, '2026-10-20', corrected)
check('rejouer la révision ne double pas les faits', total(partitions['2026-10-20']) == total(corrected))
check('témoin append cumule deux versions :2600impressions78clics', (total(ROWS + corrected)['impressions'], total(ROWS + corrected)['clicks']) == (2600, 78))
print(json.dumps({'checks': len(checks), 'passed': checks, 'complete': total(ROWS), 'visible_A': total(visible_a), 'distinct_wrong': distinct, 'corrected': total(corrected), 'scope': 'fiction locale, aucun moteur BigQuery exécuté'}, ensure_ascii=False, indent=2, default=str))
