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('fetchPaichaOverview parses overview items', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'paicha_no': 'R00001', 'total_count': 2, 'items': [ { 'zongpai_no': '26B1', 'work_order_no': '6-1(7)', 'quantity': 80, 'location_code': 'A01-02-03', 'status': 'on_shelf', }, { 'zongpai_no': '26B2', 'work_order_no': '6-2(3)', 'quantity': 45, 'location_code': null, 'status': 'not_shelved', }, ], }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.fetchPaichaOverview( baseUrl: 'http://localhost', zongpaiNo: '26B1', ); expect(result.success, isTrue); expect(result.paichaNo, 'R00001'); expect(result.totalCount, 2); expect(result.items.first.status, 'on_shelf'); expect(result.items.last.locationCode, isNull); }); test('fetchPaichaOverview maps 404 to not found result', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({'error_code': 'PAICHA_NOT_FOUND', 'message': '暂无排产信息'}), 404, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.fetchPaichaOverview( baseUrl: 'http://localhost', zongpaiNo: '26B404', ); expect(result.success, isFalse); expect(result.notFound, isTrue); expect(result.errorMessage, '暂无排产信息'); }); test( 'fetchPaichaOverview maps network failure to retryable error', () async { final mockClient = _MockClient((request) async { throw Exception('Connection refused'); }); final svc = ApiService(client: mockClient); final result = await svc.fetchPaichaOverview( baseUrl: 'http://localhost', zongpaiNo: '26B1', ); expect(result.success, isFalse); expect(result.notFound, isFalse); expect(result.errorMessage, '加载失败,点击重试'); }, ); 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 sends simplified request body', () 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': 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, ); expect(result.success, isTrue); expect(result.boxItemId, 102); expect(result.zongpaiNo, '26BW0012'); expect(body, {'zongpai_no': '26BW0012', 'box_no': 4, 'quantity': 34}); expect(body.containsKey('box_mode'), isFalse); }); test('updateBoxRecord sends simplified request body', () 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, ); expect(result.success, isTrue); expect(body, {'box_no': 4, 'quantity': 20}); expect(body.containsKey('box_mode'), isFalse); }); 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, '指定装箱明细不存在'); }); }); group('ApiService attachment APIs', () { test('fetchAttachmentCategories parses list', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode([ {'id': 1, 'name': '检验证书', 'sort_order': 1}, {'id': 2, 'name': '增发', 'sort_order': 2}, ]), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.fetchAttachmentCategories( baseUrl: 'http://localhost', ); expect(result.success, isTrue); expect(result.categories.length, 2); expect(result.categories.first.name, '检验证书'); }); test('fetchAttachmentCategories handles network failure', () async { final mockClient = _MockClient((request) async { throw Exception('Connection refused'); }); final svc = ApiService(client: mockClient); final result = await svc.fetchAttachmentCategories( baseUrl: 'http://localhost', ); expect(result.success, isFalse); expect(result.errorMessage, '网络异常,请检查网络连接'); }); test('fetchAttachmentStatus parses undetermined zongpai', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'zongpai_no': '26BW0011', 'determination': 'undetermined', 'all_complete': false, 'items': [], }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.fetchAttachmentStatus( baseUrl: 'http://localhost', zongpaiNo: '26BW0011', ); expect(result.success, isTrue); expect(result.determination, 'undetermined'); expect(result.items, isEmpty); }); test('putAttachmentConfig sends full payload', () 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({ 'zongpai_no': '26BW0011', 'determination': 'has', 'all_complete': false, 'items': [ { 'category_id': 1, 'name': '检验证书', 'expected_qty': 80, 'boxed_qty': 0, 'location_code': null, 'complete': false, }, ], }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.putAttachmentConfig( baseUrl: 'http://localhost', zongpaiNo: '26BW0011', determination: 'has', items: [AttachmentConfigItemData(categoryId: 1, expectedQty: 80)], ); expect(result.success, isTrue); expect(result.items.single.complete, isFalse); expect(body['items'], [ {'category_id': 1, 'expected_qty': 80}, ]); }); test('registerAttachmentLocation returns 200 with data', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'zongpai_no': '26BW0011', 'category_id': 1, 'location_code': 'A01-01-01', 'created_at': '2026-08-12T10:00:00', 'previous_location': null, }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.registerAttachmentLocation( baseUrl: 'http://localhost', zongpaiNo: '26BW0011', categoryId: 1, locationCode: 'A01-01-01', ); expect(result.success, isTrue); expect(result.locationCode, 'A01-01-01'); }); test('saveAttachmentBox sends request with category_id', () 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': 1, 'paichan_no': 'W00009', 'box_no': 10, 'zongpai_no': '26BW0011', 'category_id': 1, 'quantity': 30, 'created_at': '2026-08-12T10:00:00', }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.saveAttachmentBox( baseUrl: 'http://localhost', zongpaiNo: '26BW0011', categoryId: 1, boxNo: 10, quantity: 30, ); expect(result.success, isTrue); expect(result.boxItemId, 1); expect(body['category_id'], 1); }); test('saveAttachmentBox returns duplicate on 409', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ 'error_code': 'DUPLICATE_BOX_NO', 'message': '排产号 W00009 下箱号 10 已存在', 'paichan_no': 'W00009', 'box_no': 10, }), 409, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.saveAttachmentBox( baseUrl: 'http://localhost', zongpaiNo: '26BW0011', categoryId: 1, boxNo: 10, quantity: 30, ); expect(result.success, isFalse); expect(result.isDuplicate, isTrue); }); test('updateAttachmentBox sends PATCH with box_no and quantity', () 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': 1, 'paichan_no': 'W00009', 'box_no': 11, 'zongpai_no': '26BW0011', 'category_id': 1, 'quantity': 20, 'updated_at': '2026-08-12T10:00:00', }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); }); final svc = ApiService(client: mockClient); final result = await svc.updateAttachmentBox( baseUrl: 'http://localhost', boxItemId: 1, boxNo: 11, quantity: 20, ); expect(result.success, isTrue); expect(body, {'box_no': 11, 'quantity': 20}); }); test('deleteAttachmentBox 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': 1, '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.deleteAttachmentBox( baseUrl: 'http://localhost', boxItemId: 1, ); final missing = await svc.deleteAttachmentBox( 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, ); } }