import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:pad_scanner/services/api_service.dart'; void main() { group('ApiService.registerLocation', () { test('returns ok on 200', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'zongpai_no': '26B1', 'location_code': 'A01-02-03', 'created_at': '2026-05-11T10:00:00', }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.registerLocation( baseUrl: 'http://localhost', zongpaiNo: '26B1', locationCode: 'A01-02-03', ); expect(result.success, isTrue); expect(result.isDuplicate, isFalse); expect(result.isOffShelfSuccess, isFalse); }); test('returns off-shelf success on 200 with previous_location', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'zongpai_no': '26B1', 'location_code': 'TRANS-01', 'previous_location': 'A01-02-03', 'created_at': '2026-05-13T10:00:00', }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.registerLocation( baseUrl: 'http://localhost', zongpaiNo: '26B1', locationCode: 'TRANS-01', ); expect(result.success, isTrue); expect(result.isOffShelfSuccess, isTrue); expect(result.isDuplicate, isFalse); }); test( 'returns duplicate on 409 with location_code and registered_at', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'error_code': 'DUPLICATE_LOCATION', 'message': '该总排号已存在货位记录', 'location_code': 'A01-02-03', 'registered_at': '2026-05-11T10:00:00', }), 409, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.registerLocation( baseUrl: 'http://localhost', zongpaiNo: '26B1', locationCode: 'A02-01-01', ); expect(result.success, isFalse); expect(result.isDuplicate, isTrue); expect(result.duplicateInfo?['location_code'], 'A01-02-03'); expect(result.duplicateInfo?['registered_at'], '2026-05-11T10:00:00'); }, ); test('returns already off shelf on 409 ALREADY_OFF_SHELF', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'error_code': 'ALREADY_OFF_SHELF', 'message': '该总排号已下架至转运区域,不可重新上架', 'location_code': 'TRANS-01', 'registered_at': '2026-05-13T10:00:00', }), 409, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.registerLocation( baseUrl: 'http://localhost', zongpaiNo: '26B1', locationCode: 'A02-01-01', ); expect(result.success, isFalse); expect(result.isDuplicate, isFalse); expect(result.isAlreadyOffShelf, isTrue); expect(result.errorMessage, '该总排号已下架至转运区域,不可重新上架'); expect(result.conflictInfo?['location_code'], 'TRANS-01'); }); test('returns error on 400 with message', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'error_code': 'INVALID_ZONGPAI', 'message': 'INVALID_ZONGPAI', }), 400, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.registerLocation( baseUrl: 'http://localhost', zongpaiNo: 'INVALID', locationCode: 'A01-02-03', ); expect(result.success, isFalse); expect(result.isDuplicate, isFalse); expect(result.errorMessage, 'INVALID_ZONGPAI'); }); test('returns error on network failure', () async { final mockClient = _MockClient((request) async { throw Exception('Connection refused'); }); final svc = ApiService(client: mockClient, timeout: Duration(seconds: 1)); final result = await svc.registerLocation( baseUrl: 'http://localhost', zongpaiNo: '26B1', locationCode: 'A01-02-03', ); expect(result.success, isFalse); expect(result.errorMessage, '网络异常,请检查网络连接'); }); }); group('ApiService boxing APIs', () { test('fetchBoxInfo parses current boxes and box item ids', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'zongpai_no': '26BW0011', 'paichan_no': 'W00009', 'work_order_no': '6-1(7)', 'quantity': 80, 'current_zongpai_boxes': [ {'box_item_id': 101, 'box_no': 3, 'quantity': 30}, ], 'existing_boxes': [ { 'box_no': 3, 'items': [ { 'box_item_id': 101, 'zongpai_no': '26BW0011', 'work_order_no': '6-1(7)', 'quantity': 30, 'total_quantity': 80, }, ], }, ], 'max_box_no': 3, 'suggested_box_no': 4, }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.fetchBoxInfo( baseUrl: 'http://localhost', zongpaiNo: '26BW0011', ); expect(result.success, isTrue); expect(result.currentZongpaiBoxes.single.boxItemId, 101); expect(result.existingBoxes.single.items.single.boxItemId, 101); expect(result.existingBoxes.single.items.single.totalQuantity, 80); expect(result.suggestedBoxNo, 4); }); test('saveBoxRecord parses zongpai number from success response', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'box_item_id': 102, 'paichan_no': 'W00009', 'box_no': 4, 'zongpai_no': '26BW0012', 'quantity': 34, 'created_at': '2026-05-13T10:00:00', }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.saveBoxRecord( baseUrl: 'http://localhost', zongpaiNo: '26BW0012', boxNo: 4, quantity: 34, boxMode: 'many-to-one', ); expect(result.success, isTrue); expect(result.boxItemId, 102); expect(result.zongpaiNo, '26BW0012'); }); test('updateBoxRecord sends many-to-one box mode', () async { late Map body; final mockClient = _MockClient((request) async { final req = request as http.Request; body = jsonDecode(req.body) as Map; return http.Response( jsonEncode({ 'box_item_id': 102, 'paichan_no': 'W00009', 'box_no': 4, 'zongpai_no': '26BW0012', 'quantity': 20, 'updated_at': '2026-05-13T10:00:00', }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.updateBoxRecord( baseUrl: 'http://localhost', boxItemId: 102, boxNo: 4, quantity: 20, boxMode: 'many-to-one', ); expect(result.success, isTrue); expect(body['box_mode'], 'many-to-one'); }); test('deleteBoxRecord handles success and not found', () async { var calls = 0; final mockClient = _MockClient((request) async { calls += 1; if (calls == 1) { return http.Response( jsonEncode({'box_item_id': 102, 'deleted': true}), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); } return http.Response( jsonEncode({ 'error_code': 'BOX_ITEM_NOT_FOUND', 'message': '指定装箱明细不存在', }), 404, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final ok = await svc.deleteBoxRecord( baseUrl: 'http://localhost', boxItemId: 102, ); final missing = await svc.deleteBoxRecord( baseUrl: 'http://localhost', boxItemId: 999, ); expect(ok.success, isTrue); expect(missing.success, isFalse); expect(missing.errorMessage, '指定装箱明细不存在'); }); }); } class _MockClient extends http.BaseClient { final Future Function(http.BaseRequest) _handler; _MockClient(this._handler); @override Future send(http.BaseRequest request) async { final response = await _handler(request); return http.StreamedResponse( http.ByteStream.fromBytes(response.bodyBytes), response.statusCode, headers: response.headers, reasonPhrase: response.reasonPhrase, ); } }