// // TravelAlbumAIRetouchTemplateViewModelTests.swift // suixinkanTests // import XCTest @testable import suixinkan /// AI 修图模板选择状态测试。 @MainActor final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase { func testLoadDefaultsRequiredSelectionsAndLeavesAtmosphereEmpty() async { let api = makeAPI() let viewModel = TravelAlbumAIRetouchTemplateViewModel( albumId: 8, scenicId: 18, materialIds: [4, 3, 2, 1] ) await viewModel.loadTemplates(api: api) XCTAssertEqual(api.aiRetouchTemplateScenicIds, [18]) XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11) XCTAssertNil(viewModel.selectedAtmosphereTemplateId) XCTAssertEqual(viewModel.selectedCoverTemplateId, 31) XCTAssertTrue(viewModel.showsCoverTemplates) XCTAssertTrue(viewModel.canSubmit) } func testAtmosphereSelectionTogglesOffWhenTappedAgain() async { let api = makeAPI() let viewModel = TravelAlbumAIRetouchTemplateViewModel( albumId: 8, scenicId: 18, materialIds: [1] ) await viewModel.loadTemplates(api: api) viewModel.toggleTemplate(id: 21, category: .atmosphere) XCTAssertEqual(viewModel.selectedAtmosphereTemplateId, 21) viewModel.toggleTemplate(id: 21, category: .atmosphere) XCTAssertNil(viewModel.selectedAtmosphereTemplateId) } func testThreeMaterialsHideCoverAndOmitCoverFromSubmission() async { let api = makeAPI() let viewModel = TravelAlbumAIRetouchTemplateViewModel( albumId: 8, scenicId: 18, materialIds: [3, 1, 2] ) var submitted = false viewModel.onSubmitted = { submitted = true } await viewModel.loadTemplates(api: api) XCTAssertFalse(viewModel.showsCoverTemplates) XCTAssertNil(viewModel.selectedCoverTemplateId) await viewModel.submit(api: api) XCTAssertTrue(submitted) XCTAssertEqual(api.aiRetouchRequests.count, 1) XCTAssertEqual(api.aiRetouchRequests.first?.materialIds, [1, 2, 3]) XCTAssertNil(api.aiRetouchRequests.first?.aiPreviewTabs) } func testFourMaterialsSubmitAllSelectedTemplateIds() async { let api = makeAPI() let viewModel = TravelAlbumAIRetouchTemplateViewModel( albumId: 8, scenicId: 18, materialIds: [1, 2, 3, 4] ) await viewModel.loadTemplates(api: api) viewModel.toggleTemplate(id: 12, category: .refined) viewModel.toggleTemplate(id: 21, category: .atmosphere) await viewModel.submit(api: api) XCTAssertEqual( api.aiRetouchRequests.first, TravelAlbumAIRetouchRequest( userEquityTravelId: 8, materialIds: [1, 2, 3, 4], refinedTemplateId: 12, atmosphereTemplateId: 21, aiPreviewTabs: 31 ) ) } func testMissingRequiredCoverTemplateDisablesSubmission() async { let api = makeAPI() api.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse( refinedTemplates: [template(11, "清透")], atmosphereTemplates: [template(21, "暖阳")], coverTemplates: [] ) let viewModel = TravelAlbumAIRetouchTemplateViewModel( albumId: 8, scenicId: 18, materialIds: [1, 2, 3, 4] ) await viewModel.loadTemplates(api: api) XCTAssertFalse(viewModel.canSubmit) XCTAssertEqual(viewModel.validationMessage, "暂无可用的封面风格模板") } func testLoadFailureExposesRetryMessageAndKeepsSubmissionDisabled() async { let api = makeAPI() api.aiRetouchTemplatesError = APIError.serverCode(500, "模板服务繁忙") let viewModel = TravelAlbumAIRetouchTemplateViewModel( albumId: 8, scenicId: 18, materialIds: [1] ) await viewModel.loadTemplates(api: api) XCTAssertEqual(viewModel.loadErrorMessage, "模板服务繁忙") XCTAssertFalse(viewModel.canSubmit) XCTAssertTrue(viewModel.refinedTemplates.isEmpty) } func testSubmitFailureKeepsSelectionAndAllowsRetry() async { let api = makeAPI() api.submitAIRetouchError = APIError.serverCode(500, "提交服务繁忙") let viewModel = TravelAlbumAIRetouchTemplateViewModel( albumId: 8, scenicId: 18, materialIds: [1] ) var message: String? var submitted = false viewModel.onShowMessage = { message = $0 } viewModel.onSubmitted = { submitted = true } await viewModel.loadTemplates(api: api) await viewModel.submit(api: api) XCTAssertEqual(api.aiRetouchRequests.count, 1) XCTAssertEqual(message, "提交服务繁忙") XCTAssertFalse(submitted) XCTAssertFalse(viewModel.isSubmitting) XCTAssertTrue(viewModel.canSubmit) XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11) } func testSubmittingPreventsDuplicateRequest() async { let api = makeAPI() api.submitAIRetouchDelayNanoseconds = 50_000_000 let viewModel = TravelAlbumAIRetouchTemplateViewModel( albumId: 8, scenicId: 18, materialIds: [1] ) await viewModel.loadTemplates(api: api) let firstSubmission = Task { await viewModel.submit(api: api) } await waitUntil { viewModel.isSubmitting } await viewModel.submit(api: api) await firstSubmission.value XCTAssertEqual(api.aiRetouchRequests.count, 1) XCTAssertFalse(viewModel.isSubmitting) } private func makeAPI() -> TravelAlbumMockAPI { let api = TravelAlbumMockAPI() api.aiRetouchTemplatesResponse = TravelAlbumAIRetouchTemplatesResponse( refinedTemplates: [template(11, "清透"), template(12, "自然")], atmosphereTemplates: [template(21, "暖阳")], coverTemplates: [template(31, "杂志")] ) return api } private func template(_ id: Int, _ name: String) -> TravelAlbumAIRetouchTemplate { TravelAlbumAIRetouchTemplate(id: id, name: name, previewURL: "https://cdn.example.com/\(id).jpg") } private func waitUntil(_ condition: @escaping () -> Bool) async { for _ in 0 ..< 100 { if condition() { return } await Task.yield() } } }