#!/usr/bin/env python3
"""Synthetic bulk INDEX receipt checker. No network, model or database calls.

Narrow contract: explicit IDs, one concrete index, unique IDs per batch,
full unfiltered response. A retry candidate is not permission to replay.
"""
import copy
import json
import sys
import unittest


def reconcile(expected, http_status, body):
    """Return classifications only after validating the complete receipt."""
    if not expected or any(type(x) is not str or not x for x in expected):
        raise ValueError('nonempty explicit document IDs required')
    if len(set(expected)) != len(expected):
        raise ValueError('duplicate submitted document ID')
    if type(http_status) is not int or http_status != 200:
        raise ValueError('no usable bulk receipt; reconcile outcome separately')
    if type(body) is not dict or type(body.get('errors')) is not bool:
        raise ValueError('missing Boolean errors flag')
    items = body.get('items')
    if type(items) is not list or len(items) != len(expected):
        raise ValueError('receipt cardinality mismatch')
    result = {'acknowledged': [], 'retry_candidates': [], 'blocked': []}
    failed = False
    for doc_id, item in zip(expected, items):
        if type(item) is not dict or set(item) != {'index'}:
            raise ValueError('expected an index receipt')
        row = item['index']
        if type(row) is not dict or row.get('_id') != doc_id or row.get('_index') != 'rag-v7':
            raise ValueError('receipt identity mismatch')
        status = row.get('status')
        if type(status) is not int or status not in (200, 201) and not 400 <= status <= 599:
            raise ValueError('unsupported item status')
        if status in (200, 201):
            if 'error' in row:
                raise ValueError('success contains error')
            result['acknowledged'].append(doc_id)
        else:
            if type(row.get('error')) is not dict or not row['error'].get('type'):
                raise ValueError('failure lacks error detail')
            failed = True
            bucket = 'retry_candidates' if status == 429 else 'blocked'
            result[bucket].append(doc_id)
    if body['errors'] != failed:
        raise ValueError('summary contradicts item outcomes')
    return result


def fixture(statuses=(201, 429, 400)):
    ids = ['chunk-a', 'chunk-b', 'chunk-c']
    items = []
    for doc_id, status in zip(ids, statuses):
        row = {'_index': 'rag-v7', '_id': doc_id, 'status': status}
        if status >= 400:
            row['error'] = {'type': 'es_rejected_execution_exception' if status == 429 else 'mapper_parsing_exception'}
        items.append({'index': row})
    return ids, {'errors': any(s >= 400 for s in statuses), 'items': items}


class ReceiptTests(unittest.TestCase):
    def test_partial_success(self):
        ids, body = fixture()
        self.assertEqual(reconcile(ids, 200, body), {'acknowledged': ['chunk-a'], 'retry_candidates': ['chunk-b'], 'blocked': ['chunk-c']})

    def test_all_success(self):
        ids, body = fixture((200, 201, 201))
        self.assertEqual(reconcile(ids, 200, body)['acknowledged'], ids)

    def test_missing_item(self):
        ids, body = fixture(); body['items'].pop()
        with self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_reordered_items(self):
        ids, body = fixture(); body['items'].reverse()
        with self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_duplicate_submission(self):
        ids, body = fixture(); ids[1] = ids[0]
        with self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_wrong_index(self):
        ids, body = fixture(); body['items'][0]['index']['_index'] = 'rag-v6'
        with self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_false_summary(self):
        ids, body = fixture(); body['errors'] = False
        with self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_bad_status(self):
        for status in (True, '201', 202, 302, None):
            ids, body = fixture(); body['items'][0]['index']['status'] = status
            with self.subTest(status=status), self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_success_with_error(self):
        ids, body = fixture(); body['items'][0]['index']['error'] = {'type': 'failure'}
        with self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_failure_without_detail(self):
        ids, body = fixture(); del body['items'][1]['index']['error']
        with self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_unusable_envelope(self):
        ids, body = fixture()
        for status, payload in ((503, body), (200, None), (200, {})):
            with self.subTest(status=status), self.assertRaises(ValueError): reconcile(ids, status, payload)

    def test_wrong_operation(self):
        ids, body = fixture(); body['items'][0] = {'create': body['items'][0]['index']}
        with self.assertRaises(ValueError): reconcile(ids, 200, body)

    def test_conflict_and_server_error_blocked(self):
        ids, body = fixture((201, 409, 503))
        result = reconcile(ids, 200, body)
        self.assertEqual(result['blocked'], ids[1:])
        self.assertEqual(result['retry_candidates'], [])

    def test_input_unchanged(self):
        ids, body = fixture(); before = copy.deepcopy(body)
        reconcile(ids, 200, body)
        self.assertEqual(body, before)


if __name__ == '__main__':
    if '--test' in sys.argv:
        unittest.main(argv=[sys.argv[0]], verbosity=2)
    else:
        ids, body = fixture()
        print('weak_acknowledged=' + str(len(ids)))
        print(json.dumps(reconcile(ids, 200, body), sort_keys=True))
