package application import ( "testing" "time" "gorm.io/gorm" "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/page" ) // Regression: The app used to reach READY before audio downloads completed, // showing "Status: READY" while "0 / 14 Files Downloaded" was still in progress. // The fix gates READY on downloadsComplete being true. // TestStateTransitionsToDownloadingAssets verifies that when all config // prerequisites are met but downloads are not complete, the state transitions // to DOWNLOADING_ASSETS instead of READY. func TestStateTransitionsToDownloadingAssets(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "test-key-123" app.DB.Save(config) app.downloadsComplete = false downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if result != DOWNLOADING_ASSETS { t.Errorf("state = %v, want DOWNLOADING_ASSETS (downloads not complete)", result) } } // TestStateTransitionsToReadyWhenDownloadsComplete verifies that the state // transitions to READY only after downloads are complete. func TestStateTransitionsToReadyWhenDownloadsComplete(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "test-key-123" app.DB.Save(config) app.downloadsComplete = true downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if result != READY { t.Errorf("state = %v, want READY", result) } } // Regression: Download failure should prevent the app from reaching READY. // If downloads fail, the user sees an error and must restart to retry. func TestDownloadFailurePreventsReady(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "test-key-123" app.DB.Save(config) app.downloadsComplete = false app.downloadFailed = true downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if result == READY { t.Error("state should not be READY when download has failed") } } // TestStateStaysReadyOnceSet verifies that once the state reaches READY, // computeState() returns READY without checking other prerequisites. func TestStateStaysReadyOnceSet(t *testing.T) { app := &Application{ state: READY, DB: setupTestDB(t), } downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if result != READY { t.Errorf("state = %v, want READY (should not change once set)", result) } } // TestEmailMissingPreventsReady verifies that missing email blocks progress // even when network is up and downloads are complete. func TestEmailMissingPreventsReady(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "" config.LicenseKey = "test-key-123" app.DB.Save(config) app.downloadsComplete = true downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if result != NEEDS_EMAIL_ADDRESS { t.Errorf("state = %v, want NEEDS_EMAIL_ADDRESS", result) } } // TestLicenseMissingPreventsReady verifies that missing license key blocks // progress even when email and network are present. func TestLicenseMissingPreventsReady(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "" app.DB.Save(config) app.downloadsComplete = true downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if result != NEEDS_LICENSE_KEY { t.Errorf("state = %v, want NEEDS_LICENSE_KEY", result) } } // TestDownloadDoneChannelSetsDownloadsComplete verifies that receiving on // the DownloadDone channel sets downloadsComplete to true. func TestDownloadDoneChannelSetsDownloadsComplete(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "test-key-123" app.DB.Save(config) app.downloadsComplete = false downloadDone := make(chan struct{}) close(downloadDone) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if !app.downloadsComplete { t.Error("downloadsComplete should be true after DownloadDone channel is closed") } if result != READY { t.Errorf("state = %v, want READY after downloads complete", result) } } // TestPrerequisitePriorityEmailBeforeLicense verifies that missing email // is reported before missing license key, even when both are absent. func TestPrerequisitePriorityEmailBeforeLicense(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "" config.LicenseKey = "" app.DB.Save(config) app.downloadsComplete = true downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if result != NEEDS_EMAIL_ADDRESS { t.Errorf("state = %v, want NEEDS_EMAIL_ADDRESS (email should be checked before license)", result) } } // TestPrerequisitePriorityLicenseAfterEmail verifies that when email is // present but license is missing, the state is NEEDS_LICENSE_KEY. func TestPrerequisitePriorityLicenseAfterEmail(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "" app.DB.Save(config) app.downloadsComplete = true downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } result := app.computeState() if result != NEEDS_LICENSE_KEY { t.Errorf("state = %v, want NEEDS_LICENSE_KEY", result) } } // TestStateBlocksOnDownloadUntilChannelClosed verifies that computeState // returns DOWNLOADING_ASSETS repeatedly until DownloadDone is closed. func TestStateBlocksOnDownloadUntilChannelClosed(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "test-key-123" app.DB.Save(config) app.downloadsComplete = false downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } // Should return DOWNLOADING_ASSETS multiple times before channel closes. for i := 0; i < 5; i++ { result := app.computeState() if result != DOWNLOADING_ASSETS { t.Fatalf("iteration %d: state = %v, want DOWNLOADING_ASSETS", i, result) } } // Now close the channel. close(downloadDone) result := app.computeState() if result != READY { t.Errorf("after close: state = %v, want READY", result) } } // TestDownloadDoneNonBlocking verifies that computeState does not block // when DownloadDone channel is open (non-blocking select). func TestDownloadDoneNonBlocking(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "test-key-123" app.DB.Save(config) app.downloadsComplete = false downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } // computeState must return promptly even though DownloadDone is not closed. done := make(chan struct{}) go func() { result := app.computeState() if result != DOWNLOADING_ASSETS { t.Errorf("state = %v, want DOWNLOADING_ASSETS", result) } close(done) }() select { case <-done: // Good: returned promptly. case <-time.After(2 * time.Second): t.Fatal("computeState blocked on DownloadDone channel (should be non-blocking)") } } // TestMultipleComputeStateCallsAfterDownloadComplete verifies that calling // computeState repeatedly after downloads finish does not panic or change state. func TestMultipleComputeStateCallsAfterDownloadComplete(t *testing.T) { app := &Application{ state: FIRST_RUN, DB: setupTestDB(t), } networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { t.Fatal(err) } config.EmailAddress = "test@example.com" config.LicenseKey = "test-key-123" app.DB.Save(config) downloadDone := make(chan struct{}) close(downloadDone) app.PageRouter = &page.Router{ Context: page.PageCtx{ DbPtr: app.DB, DownloadDone: downloadDone, }, } // First call transitions to READY. result := app.computeState() if result != READY { t.Fatalf("first call: state = %v, want READY", result) } app.state = result // Subsequent calls should return READY without panic. for i := 0; i < 10; i++ { result = app.computeState() if result != READY { t.Fatalf("iteration %d: state = %v, want READY", i, result) } } } func setupTestDB(t *testing.T) *gorm.DB { t.Helper() d, err := db.Connect(":memory:") if err != nil { t.Fatal(err) } if err := db.Migrate(d); err != nil { t.Fatal(err) } cfg := &db.AppConfig{ Model: gorm.Model{ID: 1}, DataDir: t.TempDir(), AssetsDir: t.TempDir(), SystemLevel: "BASIC", SelectedAudioDevice: "default", InitialFontSize: 128, StandardFontSize: 32, LinePattern: "fibonacci", AudioEnabled: true, SavedVolume: 1.0, EmailAddress: "", LicenseKey: "", } if err := d.Create(cfg).Error; err != nil { t.Fatal(err) } return d }