<?php

declare(strict_types=1);

require __DIR__ . '/contract_shape.php';

$dialect = getenv('POC_DIALECT');
$baseUrl = rtrim((string)getenv('POC_BASE_URL'), '/');
if (!$baseUrl || !in_array($dialect, ['mysql', 'dm'], true)) exit(2);
$host = getenv('POC_HOST');
$port = getenv('POC_PORT');
$schema = getenv('POC_SCHEMA');
$dsn = $dialect === 'dm'
    ? "dm:host=$host;port=$port;schema=$schema"
    : "mysql:host=$host;port=$port;dbname=$schema;charset=utf8mb4";
$pdo = new PDO($dsn, getenv('POC_USER'), getenv('POC_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$equipmentTable = $dialect === 'dm' ? 'YDY_EQUIPMENT' : 'ydy_equipment';
$restaurantTable = $dialect === 'dm' ? 'YDY_RESTAURANTS' : 'ydy_restaurants';
$eventTable = $dialect === 'dm' ? 'YDY_DEVICE_WEIGHT_EVENT' : 'ydy_device_weight_event';
$operatorName = (string)(getenv('POC_OPERATOR_NAME') ?: '兼容测🙂');
$decimalValue = static function ($value): string {
    $value = (string)$value;
    if (!preg_match('/^-?[0-9]+(?:\.[0-9]+)?$/D', $value)) {
        throw new RuntimeException('Unexpected stored DECIMAL representation');
    }
    if (strpos($value, '.') !== false) {
        $value = rtrim(rtrim($value, '0'), '.');
    }
    return $value === '-0' ? '0' : $value;
};
$pdo->query('SELECT COUNT(*) FROM ' . $eventTable . ' WHERE 1=0');
$candidate = $pdo->query('SELECT E.ID,E.CODE,E.RESTAURANT_ID FROM ' . $equipmentTable . ' E'
    . ' JOIN ' . $restaurantTable . ' R ON E.RESTAURANT_ID=R.ID'
    . " WHERE E.DEL_FLAG=0 AND E.TYPE IN (2,3,4,10) AND E.CODE<>'' AND R.IS_DELETE=2"
    . ' ORDER BY E.ID '
    . ($dialect === 'dm' ? 'FETCH FIRST 1 ROWS ONLY' : 'LIMIT 1'));
$equipment = array_change_key_case($candidate->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
if (!$equipment || (int)$equipment['id'] <= 0) {
    throw new RuntimeException('Weighing equipment fixture missing');
}
$eventId = 'CDXWT' . strtoupper(bin2hex(random_bytes(7)));
$largeEventId = 'CDXWT' . strtoupper(bin2hex(random_bytes(7)));
$invalidEventId = 'CDXWT' . strtoupper(bin2hex(random_bytes(7)));
$emptyNameEventId = 'CDXWT' . strtoupper(bin2hex(random_bytes(7)));
$nullNameEventId = 'CDXWT' . strtoupper(bin2hex(random_bytes(7)));
$payload = [
    'event_id' => $eventId,
    'event_type' => 'non_pick_change',
    'event_time' => date('Y-m-d H:i:s'),
    'trigger_source' => 'idle_weight_change',
    'device_code' => (string)$equipment['code'],
    'before_weight' => '100.00',
    'after_weight' => '95.00',
    'change_weight' => '-5.00',
    'unit' => 'g',
    'operator_type' => 'system',
    'retry_count' => 0,
];
$contracts = [];
$post = static function (string $route, array $form) use ($baseUrl, &$contracts): array {
    $context = stream_context_create(['http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
        'content' => http_build_query($form, '', '&', PHP_QUERY_RFC3986),
        'timeout' => 15, 'ignore_errors' => true,
    ]]);
    $body = file_get_contents($baseUrl . $route, false, $context);
    $response = is_string($body) ? json_decode($body, true) : null;
    if (!is_array($response)) throw new RuntimeException('Weight device HTTP response was not JSON');
    $inspectTree = $route === '/api/device/restaurant/tree'
        && !empty($form['equipment_code']);
    $contracts[] = ['route' => $route,
        'shape' => pocJsonContractSignature($body, $inspectTree)];
    return $response;
};
$postJson = static function (string $route, array $payload) use ($baseUrl, &$contracts): array {
    $content = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if (!is_string($content)) throw new RuntimeException('JSON fixture encoding failed');
    $context = stream_context_create(['http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\n",
        'content' => $content, 'timeout' => 15, 'ignore_errors' => true,
    ]]);
    $body = file_get_contents($baseUrl . $route, false, $context);
    $response = is_string($body) ? json_decode($body, true) : null;
    if (!is_array($response)) throw new RuntimeException('Weight JSON HTTP response was not JSON');
    $contracts[] = ['route' => $route, 'shape' => pocJsonContractSignature($body)];
    return $response;
};
$find = $pdo->prepare('SELECT ID,EQUIPMENT_ID,RESTAURANT_ID,BEFORE_WEIGHT,AFTER_WEIGHT,CHANGE_WEIGHT'
    . ' FROM ' . $eventTable . ' WHERE EVENT_ID=?');
try {
    $first = $post('/api/device/weight/upload', $payload);
    $find->execute([$eventId]);
    $rows = array_map('array_change_key_case', $find->fetchAll(PDO::FETCH_ASSOC));
    if ((int)($first['code'] ?? -1) !== 0 || count($rows) !== 1
        || (int)$rows[0]['equipment_id'] !== (int)$equipment['id']
        || (int)$rows[0]['restaurant_id'] !== (int)$equipment['restaurant_id']
        || $decimalValue($rows[0]['before_weight']) !== '100'
        || $decimalValue($rows[0]['after_weight']) !== '95'
        || $decimalValue($rows[0]['change_weight']) !== '-5') {
        throw new RuntimeException('Weight event insert/database readback mismatch');
    }
    $duplicate = $post('/api/device/weight/upload', $payload);
    $find->execute([$eventId]);
    if ((int)($duplicate['code'] ?? -1) !== 0
        || count($find->fetchAll(PDO::FETCH_ASSOC)) !== 1) {
        throw new RuntimeException('Weight event duplicate created a second row');
    }
    $invalid = $payload;
    unset($invalid['event_type']);
    $bad = $post('/api/device/weight/upload', $invalid);
    $find->execute([$eventId]);
    if ((int)($bad['code'] ?? 0) === 0
        || count($find->fetchAll(PDO::FETCH_ASSOC)) !== 1) {
        throw new RuntimeException('Invalid weight event was accepted');
    }
    $readName = $pdo->prepare('SELECT OPERATOR_NAME FROM ' . $eventTable . ' WHERE EVENT_ID=?');
    $emptyName = $payload;
    $emptyName['event_id'] = $emptyNameEventId;
    $emptyName['operator_name'] = '';
    $emptyResponse = $post('/api/device/weight/upload', $emptyName);
    $readName->execute([$emptyNameEventId]);
    $emptyStored = $readName->fetchColumn();
    $nullName = $payload;
    $nullName['event_id'] = $nullNameEventId;
    $nullName['operator_name'] = null;
    $nullResponse = $postJson('/api/device/weight/upload', $nullName);
    $readName->execute([$nullNameEventId]);
    $nullStored = $readName->fetchColumn();
    if ((int)($emptyResponse['code'] ?? -1) !== 0
        || (int)($nullResponse['code'] ?? -1) !== 0
        || $emptyStored !== '' || $nullStored !== '') {
        throw new RuntimeException('Optional operator_name empty/null roundtrip changed');
    }
    $largePayload = $payload;
    $largePayload['event_id'] = $largeEventId;
    $largePayload['operator_name'] = $operatorName;
    $largePayload['event_type'] = 'scale_abnormal';
    $largePayload['trigger_source'] = 'scale_error';
    $largePayload['before_weight'] = '100.125';
    $largePayload['after_weight'] = '99.995';
    $largePayload['change_weight'] = '-0.130';
    $largePayload['compat_padding'] = str_repeat('测', 12000);
    $largeResponse = $post('/api/device/weight/upload', $largePayload);
    $readLarge = $pdo->prepare('SELECT BEFORE_WEIGHT,AFTER_WEIGHT,CHANGE_WEIGHT,OPERATOR_NAME,RAW_PAYLOAD'
        . ' FROM ' . $eventTable . ' WHERE EVENT_ID=?');
    $readLarge->execute([$largeEventId]);
    $largeRow = array_change_key_case($readLarge->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    $rawPayload = $largeRow['raw_payload'] ?? '';
    if (is_resource($rawPayload)) $rawPayload = stream_get_contents($rawPayload);
    $decodedRaw = json_decode((string)$rawPayload, true);
    if ((int)($largeResponse['code'] ?? -1) !== 0
        || (string)($largeRow['operator_name'] ?? '') !== $operatorName
        || $decimalValue($largeRow['before_weight'] ?? '') !== '100.13'
        || $decimalValue($largeRow['after_weight'] ?? '') !== '100'
        || $decimalValue($largeRow['change_weight'] ?? '') !== '-0.13'
        || strlen((string)$rawPayload) <= 32768
        || !is_array($decodedRaw)
        || (string)($decodedRaw['compat_padding'] ?? '') !== $largePayload['compat_padding']) {
        throw new RuntimeException('Large UTF-8 JSON/decimal weight event readback mismatch: '
            . json_encode([
                'code' => $largeResponse['code'] ?? null,
                'message' => mb_substr((string)($largeResponse['message'] ?? ''), 0, 80),
                'operator_equal' => (string)($largeRow['operator_name'] ?? '') === $operatorName,
                'operator_hex' => bin2hex((string)($largeRow['operator_name'] ?? '')),
                'raw_bytes' => strlen((string)$rawPayload),
                'json_valid' => is_array($decodedRaw),
                'padding_equal' => (string)($decodedRaw['compat_padding'] ?? '') === $largePayload['compat_padding'],
            ]));
    }
    $badRetry = $payload;
    $badRetry['event_id'] = $invalidEventId;
    $badRetry['retry_count'] = '-1';
    $badRetryResponse = $post('/api/device/weight/upload', $badRetry);
    $find->execute([$invalidEventId]);
    if ((int)($badRetryResponse['code'] ?? 0) === 0
        || $find->fetch(PDO::FETCH_ASSOC)) {
        throw new RuntimeException('Negative retry count was accepted');
    }
    echo json_encode(['dialect' => $dialect, 'status' => 'passed',
        'operator_and_large_json_roundtrip' => true,
        'raw_payload_bytes' => strlen((string)$rawPayload),
        'persisted_decimals' => [
            (string)$largeRow['before_weight'], (string)$largeRow['after_weight'],
            (string)$largeRow['change_weight'],
        ], 'negative_retry_rejected' => true]) . PHP_EOL;
    $root = (string)getenv('POC_TARGET_ROOT');
    if ($root === '' || !is_file($root . '/application/base/Security.php')) exit(2);
    require_once $root . '/application/base/Security.php';
    $adminToken = \app\base\Security::encrypt(implode("\t", [
        '1', 'CDX-ADMIN', 'CODEX', '0', '', '', 'CODEX',
    ]));
    $adminPost = static function (string $route, array $form, string $token)
        use ($baseUrl, &$contracts): array {
        $headers = "Content-Type: application/x-www-form-urlencoded\r\n";
        if ($token !== '') $headers .= 'token: ' . $token . "\r\n";
        $context = stream_context_create(['http' => [
            'method' => 'POST', 'header' => $headers,
            'content' => http_build_query($form, '', '&', PHP_QUERY_RFC3986),
            'timeout' => 15, 'ignore_errors' => true,
        ]]);
        $body = file_get_contents($baseUrl . $route, false, $context);
        $response = is_string($body) ? json_decode($body, true) : null;
        if (!is_array($response)) throw new RuntimeException('Admin weight HTTP response not JSON');
        $contracts[] = ['route' => $route, 'shape' => pocJsonContractSignature($body)];
        return $response;
    };
    $detailRoute = '/p/deviceWeightEvent/detail';
    $detail = $adminPost($detailRoute, ['event_id' => $largeEventId], $adminToken);
    $detailData = (array)($detail['data'] ?? []);
    if ((int)($detail['code'] ?? -1) !== 0
        || (string)($detailData['event_id'] ?? '') !== $largeEventId
        || (string)($detailData['before_weight'] ?? '') !== '100.13'
        || (string)($detailData['after_weight'] ?? '') !== '100.00'
        || (string)($detailData['change_weight'] ?? '') !== '-0.13') {
        throw new RuntimeException('Admin weight detail decimal response contract mismatch: '
            . json_encode(['code' => $detail['code'] ?? null,
                'before' => $detailData['before_weight'] ?? null,
                'after' => $detailData['after_weight'] ?? null,
                'change' => $detailData['change_weight'] ?? null]));
    }
    $notLoggedIn = $adminPost($detailRoute, ['event_id' => $largeEventId], '');
    if ((int)($notLoggedIn['code'] ?? 0) === 0) {
        throw new RuntimeException('Admin weight detail did not require login');
    }
    $list = $adminPost('/p/deviceWeightEvent/list', [
        'device_code' => (string)$equipment['code'],
        'start_time' => date('Y-m-d 00:00:00'), 'page_size' => 10,
    ], $adminToken);
    $listRows = (array)($list['data']['list'] ?? []);
    $matched = array_values(array_filter($listRows, static function ($row) use ($largeEventId) {
        return (string)($row['event_id'] ?? '') === $largeEventId;
    }));
    if ((int)($list['code'] ?? -1) !== 0
        || count($matched) !== 1
        || (string)($matched[0]['after_weight'] ?? '') !== '100.00') {
        throw new RuntimeException('Admin weight list response contract mismatch');
    }
    $abnormal = $adminPost('/p/deviceWeightEvent/abnormalList', [
        'device_code' => (string)$equipment['code'],
        'start_time' => date('Y-m-d 00:00:00'), 'page_size' => 10,
    ], $adminToken);
    $abnormalRows = (array)($abnormal['data']['list'] ?? []);
    $abnormalMatched = array_values(array_filter($abnormalRows, static function ($row) use ($largeEventId) {
        return (string)($row['event_id'] ?? '') === $largeEventId;
    }));
    if ((int)($abnormal['code'] ?? -1) !== 0 || count($abnormalMatched) !== 1) {
        throw new RuntimeException('Admin abnormal weight list missing test event');
    }
    $options = $adminPost('/p/deviceWeightEvent/options', [], $adminToken);
    if ((int)($options['code'] ?? -1) !== 0) {
        throw new RuntimeException('Admin weight options HTTP failed');
    }
    echo json_encode(['dialect' => $dialect, 'status' => 'passed',
        'admin_detail_list_abnormal_options' => true,
        'fixed_decimal_strings' => true, 'missing_login_rejected' => true]) . PHP_EOL;
    echo json_encode(['dialect' => $dialect, 'status' => 'passed',
        'event_id' => $eventId, 'insert_duplicate_invalid' => true,
        'database_rows' => 1, 'optional_name_empty_null_roundtrip' => true]) . PHP_EOL;
    $tree = $post('/api/device/restaurant/tree', [
        'equipment_code' => (string)$equipment['code'],
    ]);
    $equipmentList = $post('/api/device/restaurant/equipmentList', [
        'restaurant_id' => (int)$equipment['restaurant_id'],
    ]);
    $listedIds = array_map('intval', array_column((array)($equipmentList['data']['list'] ?? []), 'equipment_id'));
    if ((int)($tree['code'] ?? -1) !== 0
        || (string)($tree['data']['equipment']['code'] ?? '') !== (string)$equipment['code']
        || (int)($tree['data']['equipment']['restaurant_id'] ?? 0) !== (int)$equipment['restaurant_id']
        || (int)($equipmentList['code'] ?? -1) !== 0
        || (int)($equipmentList['data']['restaurant']['restaurant_id'] ?? 0) !== (int)$equipment['restaurant_id']
        || !in_array((int)$equipment['id'], $listedIds, true)) {
        throw new RuntimeException('Device restaurant tree/equipment list differs from database');
    }
    $badTree = $post('/api/device/restaurant/tree', ['equipment_code' => '']);
    $badList = $post('/api/device/restaurant/equipmentList', ['restaurant_id' => 0]);
    if ((int)($badTree['code'] ?? 0) === 0 || (int)($badList['code'] ?? 0) === 0) {
        throw new RuntimeException('Device restaurant invalid inputs were accepted');
    }
    echo json_encode(['dialect' => $dialect, 'status' => 'passed',
        'restaurant_tree_and_equipment_list' => true,
        'equipment_id_matches_database' => true, 'invalid_rejected' => true]) . PHP_EOL;
    echo json_encode(['dialect' => $dialect, 'contract_shapes' => $contracts]) . PHP_EOL;
} finally {
    $pdo->prepare('DELETE FROM ' . $eventTable . ' WHERE EVENT_ID=?')->execute([$eventId]);
    $pdo->prepare('DELETE FROM ' . $eventTable . ' WHERE EVENT_ID=?')->execute([$largeEventId]);
    $pdo->prepare('DELETE FROM ' . $eventTable . ' WHERE EVENT_ID=?')->execute([$invalidEventId]);
    $pdo->prepare('DELETE FROM ' . $eventTable . ' WHERE EVENT_ID=?')->execute([$emptyNameEventId]);
    $pdo->prepare('DELETE FROM ' . $eventTable . ' WHERE EVENT_ID=?')->execute([$nullNameEventId]);
    $find->execute([$eventId]);
    if ($find->fetch(PDO::FETCH_ASSOC)) {
        throw new RuntimeException('Synthetic weight event cleanup left a row');
    }
    $find->execute([$largeEventId]);
    if ($find->fetch(PDO::FETCH_ASSOC)) {
        throw new RuntimeException('Large weight event cleanup left a row');
    }
}
