package calc import ( "errors" "testing" ) func TestNewEngine_Defaults(t *testing.T) { e := NewEngine() if e.CurrentBase() != BaseDEC { t.Errorf("expected DEC, got %s", e.CurrentBase()) } if got := e.FormatForDisplay(); got != "0" { t.Errorf("initial display: want 0, got %s", got) } } func TestCycleBase_IntegerOK(t *testing.T) { e := NewEngine() e.accumulator = 42 e.entry = "" if err := e.CycleBase(); err != nil { t.Fatalf("unexpected error: %v", err) } if e.CurrentBase() != BaseHEX { t.Errorf("expected HEX, got %s", e.CurrentBase()) } if got := e.FormatForDisplay(); got != "2A" { t.Errorf("hex of 42: want 2A, got %s", got) } } func TestCycleBase_FractionalCERR(t *testing.T) { e := NewEngine() // 23/6 = 3.8333... e.accumulator = 23.0 / 6.0 e.entry = "" err := e.CycleBase() if !errors.Is(err, ErrConversionNotPossible) { t.Fatalf("expected ErrConversionNotPossible, got %v", err) } if e.CurrentBase() != BaseDEC { t.Error("base must remain DEC after CERR") } } func TestBasicArithmetic(t *testing.T) { e := NewEngine() // 2 + 3 = e.EnterDigit('2') e.SetOperator("+") e.EnterDigit('3') e.Equals() if got := e.FormatForDisplay(); got != "5" { t.Errorf("2+3: want 5, got %s", got) } // * 4 = e.SetOperator("*") e.EnterDigit('4') e.Equals() if got := e.FormatForDisplay(); got != "20" { t.Errorf("5*4: want 20, got %s", got) } } func TestMOD(t *testing.T) { e := NewEngine() e.EnterDigit('2') e.EnterDigit('3') e.SetOperator("mod") e.EnterDigit('6') e.Equals() if got := e.FormatForDisplay(); got != "5" { t.Errorf("23 mod 6: want 5, got %s", got) } } func TestFractionalThenBASE_CERR(t *testing.T) { e := NewEngine() // 1 / 3 = 0.333... e.EnterDigit('1') e.SetOperator("/") e.EnterDigit('3') e.Equals() if e.IsInteger() { t.Error("1/3 should not be integer") } err := e.CycleBase() if !errors.Is(err, ErrConversionNotPossible) { t.Fatalf("expected CERR on fractional BASE, got %v", err) } } func TestClearAndBackspace(t *testing.T) { e := NewEngine() e.EnterDigit('1') e.EnterDigit('2') e.EnterDigit('3') e.Backspace() if got := e.FormatForDisplay(); got != "12" { t.Errorf("backspace: want 12, got %s", got) } e.ClearEntry() if got := e.FormatForDisplay(); got != "0" { t.Errorf("clear entry: want 0, got %s", got) } e.AllClear() if e.CurrentBase() != BaseDEC { t.Error("all clear should reset base to DEC for MVP") } } func TestChangeSign(t *testing.T) { e := NewEngine() e.EnterDigit('5') e.ChangeSign() if got := e.FormatForDisplay(); got != "-5" { t.Errorf("sign: want -5, got %s", got) } } // More tests (entry buffer, multi-op, decimal point, etc.) will be added as the // TUI spike exercises real usage. The CERR path is the critical one for BASE. // === Tight-scope HEX entry tests === func TestEnterDigit_HEX(t *testing.T) { e := NewEngine() e.base = BaseHEX e.EnterDigit('1') e.EnterDigit('a') e.EnterDigit('F') if got := e.FormatForDisplay(); got != "1AF" { t.Errorf("HEX entry display: want 1AF, got %s", got) } // Commit and check value (1AF hex = 431 decimal) e.SetOperator("+") e.EnterDigit('1') // simple commit e.Equals() if got := e.FormatForDisplay(); got != "432" { t.Errorf("after hex commit +1: want 432, got %s", got) } } func TestEnterDigit_HEX_IgnoresDecimal(t *testing.T) { e := NewEngine() e.base = BaseHEX e.EnterDigit('1') e.EnterDecimalPoint() // should be ignored e.EnterDigit('A') if got := e.FormatForDisplay(); got != "1A" { t.Errorf("HEX should ignore dot: want 1A, got %s", got) } } func TestEnterDigit_HEX_OnlyInHEXMode(t *testing.T) { e := NewEngine() // starts in DEC e.EnterDigit('1') e.EnterDigit('A') // 'A' ignored in DEC (now validated) if got := e.FormatForDisplay(); got != "1" { t.Errorf("A ignored in DEC: want 1, got %s", got) } e.ClearEntry() e.base = BaseHEX e.EnterDigit('A') if got := e.FormatForDisplay(); got != "A" { t.Errorf("A accepted in HEX: want A, got %s", got) } } func TestCycleBase_DuringHEXEntry(t *testing.T) { e := NewEngine() e.base = BaseHEX e.EnterDigit('1') e.EnterDigit('0') // 16 decimal // Tab while in entry (from HEX): commit the hex value (16) then switch to next (BIN in this case) err := e.CycleBase() if err != nil { t.Fatalf("unexpected CERR during integer hex entry: %v", err) } if e.CurrentBase() != BaseBIN { t.Errorf("expected BIN after one cycle from HEX, got %s", e.CurrentBase()) } // Display shows the committed value in the new base if got := e.FormatForDisplay(); got != "10000" { t.Errorf("committed hex value (16) shown in BIN: want 10000, got %s", got) } // Verify the *numeric* value was correctly parsed from hex entry (16 decimal) // by doing arithmetic and checking (even though display is in BIN) e.SetOperator("+") e.EnterDigit('1') e.Equals() // 16 + 1 = 17. In BIN that's 10001 if got := e.FormatForDisplay(); got != "10001" { t.Errorf("after hex-committed +1 (display in BIN): want 10001, got %s", got) } }