# Admin Trade Control - Test Plan

## Test Objectives

Verify that the Admin Trade Control feature:
1. ✅ Blocks new trade entries when paused
2. ✅ Allows existing positions to continue lifecycle
3. ✅ Only allows admin users to control trading state
4. ✅ Persists state across restarts
5. ✅ Provides accurate UI feedback
6. ✅ Handles errors gracefully

## Backend Tests

### Unit Tests

#### HealthTracker Tests

**File**: `bytelyst-trading-bot-service/src/services/healthTracker.test.ts`

```typescript
import { HealthTracker } from './healthTracker';

describe('HealthTracker - Trading Control', () => {
    let tracker: HealthTracker;

    beforeEach(() => {
        tracker = new HealthTracker();
    });

    test('should default to RUNNING mode', () => {
        const snapshot = tracker.getSnapshot();
        expect(snapshot.tradingControl.mode).toBe('RUNNING');
        expect(tracker.isPaused()).toBe(false);
    });

    test('should pause trading when mode set to PAUSED', () => {
        tracker.recordTradingControl({
            mode: 'PAUSED',
            lastChangedBy: 'admin@test.com',
            lastChangedAt: Date.now(),
            reason: 'Test pause'
        });
        expect(tracker.isPaused()).toBe(true);
    });

    test('should resume trading when mode set to RUNNING', () => {
        tracker.recordTradingControl({ mode: 'PAUSED', lastChangedBy: 'admin', lastChangedAt: Date.now() });
        tracker.recordTradingControl({ mode: 'RUNNING', lastChangedBy: 'admin', lastChangedAt: Date.now() });
        expect(tracker.isPaused()).toBe(false);
    });

    test('should record who changed the state', () => {
        const userId = 'admin@test.com';
        tracker.recordTradingControl({
            mode: 'PAUSED',
            lastChangedBy: userId,
            lastChangedAt: Date.now()
        });
        const snapshot = tracker.getSnapshot();
        expect(snapshot.tradingControl.lastChangedBy).toBe(userId);
    });

    test('should record timestamp of change', () => {
        const now = Date.now();
        tracker.recordTradingControl({
            mode: 'PAUSED',
            lastChangedBy: 'admin',
            lastChangedAt: now
        });
        const snapshot = tracker.getSnapshot();
        expect(snapshot.tradingControl.lastChangedAt).toBe(now);
    });
});
```

#### AutoTrader Tests

**File**: `bytelyst-trading-bot-service/src/services/AutoTrader.test.ts`

```typescript
import { AutoTrader } from './AutoTrader';
import { healthTracker } from './healthTracker';

describe('AutoTrader - Pause Enforcement', () => {
    let autoTrader: AutoTrader;
    let mockExecutor: any;
    let mockExchange: any;

    beforeEach(() => {
        mockExecutor = {
            getActivePositions: jest.fn(() => []),
            getOpenPositionCount: jest.fn(() => 0),
            checkCooldown: jest.fn(() => false),
            openPosition: jest.fn()
        };
        mockExchange = {
            getPosition: jest.fn(() => null)
        };
        autoTrader = new AutoTrader(mockExecutor, mockExchange);
        
        // Reset to RUNNING
        healthTracker.recordTradingControl({
            mode: 'RUNNING',
            lastChangedBy: 'system',
            lastChangedAt: Date.now()
        });
    });

    test('should block entry when paused', async () => {
        healthTracker.recordTradingControl({
            mode: 'PAUSED',
            lastChangedBy: 'admin',
            lastChangedAt: Date.now()
        });

        const result = { signal: 'BUY', passed: true };
        const context = { currentPrice: 50000 };
        
        await autoTrader.handleSignal('BTC/USDT', result, context);
        
        expect(mockExecutor.openPosition).not.toHaveBeenCalled();
    });

    test('should allow entry when running', async () => {
        const result = { signal: 'BUY', passed: true };
        const context = { currentPrice: 50000 };
        
        await autoTrader.handleSignal('BTC/USDT', result, context);
        
        // Entry logic should proceed (may be blocked by other checks)
        // At minimum, pause check should not block
    });

    test('should still close positions when paused', async () => {
        healthTracker.recordTradingControl({
            mode: 'PAUSED',
            lastChangedBy: 'admin',
            lastChangedAt: Date.now()
        });

        const activePosition = {
            side: 'BUY',
            entryPrice: 50000,
            size: 1,
            peakPrice: 51000
        };
        mockExecutor.getActivePositions.mockReturnValue([activePosition]);
        mockExecutor.closePosition = jest.fn();

        const result = { signal: 'SELL', passed: true };
        const context = { currentPrice: 51000 };
        
        await autoTrader.handleSignal('BTC/USDT', result, context);
        
        expect(mockExecutor.closePosition).toHaveBeenCalled();
    });
});
```

#### TradeExecutor Tests

**File**: `bytelyst-trading-bot-service/src/services/TradeExecutor.test.ts`

```typescript
import { TradeExecutor } from './TradeExecutor';
import { healthTracker } from './healthTracker';

describe('TradeExecutor - Pause Enforcement', () => {
    let executor: TradeExecutor;
    let mockExchange: any;

    beforeEach(() => {
        mockExchange = {
            placeOrder: jest.fn()
        };
        executor = new TradeExecutor(mockExchange);
        
        healthTracker.recordTradingControl({
            mode: 'RUNNING',
            lastChangedBy: 'system',
            lastChangedAt: Date.now()
        });
    });

    test('should block openPosition when paused', async () => {
        healthTracker.recordTradingControl({
            mode: 'PAUSED',
            lastChangedBy: 'admin',
            lastChangedAt: Date.now()
        });

        const result = await executor.openPosition('BTC/USDT', 'BUY', 1, 'market', 50000);
        
        expect(result.success).toBe(false);
        expect(result.error).toContain('paused');
        expect(mockExchange.placeOrder).not.toHaveBeenCalled();
    });

    test('should allow openPosition when running', async () => {
        mockExchange.placeOrder.mockResolvedValue({
            id: 'order-123',
            status: 'filled',
            filled_avg_price: 50000
        });

        const result = await executor.openPosition('BTC/USDT', 'BUY', 1, 'market', 50000);
        
        expect(mockExchange.placeOrder).toHaveBeenCalled();
    });
});
```

### Integration Tests

**File**: `bytelyst-trading-bot-service/tests/integration/tradingControl.test.ts`

```typescript
describe('Trading Control Integration', () => {
    test('pause → no new entries → resume → entries allowed', async () => {
        // 1. Pause trading
        await request(app)
            .post('/internal/trading/pause')
            .set('Authorization', `Bearer ${adminToken}`)
            .send({ reason: 'Integration test' })
            .expect(200);

        // 2. Attempt to place entry (should be blocked)
        const entryResult = await autoTrader.handleSignal('BTC/USDT', buySignal, context);
        expect(entryResult).toBeUndefined(); // blocked

        // 3. Resume trading
        await request(app)
            .post('/internal/trading/resume')
            .set('Authorization', `Bearer ${adminToken}`)
            .send({ reason: 'Integration test' })
            .expect(200);

        // 4. Attempt to place entry (should succeed)
        const entryResult2 = await autoTrader.handleSignal('BTC/USDT', buySignal, context);
        expect(entryResult2).toBeDefined(); // allowed
    });

    test('paused state persists across restart', async () => {
        // 1. Pause trading
        await request(app)
            .post('/internal/trading/pause')
            .set('Authorization', `Bearer ${adminToken}`)
            .send({ reason: 'Persistence test' })
            .expect(200);

        // 2. Simulate restart (reload state)
        apiServer.loadState();

        // 3. Verify still paused
        const status = await request(app)
            .get('/internal/trading/status')
            .set('Authorization', `Bearer ${adminToken}`)
            .expect(200);

        expect(status.body.mode).toBe('PAUSED');
    });
});
```

## Frontend Tests

### Component Tests

**File**: `bytelyst-trading-dashboard-web/src/tabs/AdminTab.test.tsx`

```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AdminTab } from './AdminTab';

describe('AdminTab - Trading Control', () => {
    const mockBotState = {
        health: {
            tradingControl: {
                mode: 'RUNNING',
                lastChangedBy: 'system',
                lastChangedAt: Date.now()
            }
        },
        settings: { enabledRules: [] }
    };

    test('should show running status when mode is RUNNING', () => {
        render(<AdminTab botState={mockBotState} />);
        expect(screen.getByText(/AUTO-TRADING: RUNNING/i)).toBeInTheDocument();
    });

    test('should show paused status when mode is PAUSED', () => {
        const pausedState = {
            ...mockBotState,
            health: {
                tradingControl: {
                    mode: 'PAUSED',
                    lastChangedBy: 'admin@test.com',
                    lastChangedAt: Date.now()
                }
            }
        };
        render(<AdminTab botState={pausedState} />);
        expect(screen.getByText(/AUTO-TRADING: PAUSED/i)).toBeInTheDocument();
    });

    test('should disable pause button when already paused', () => {
        const pausedState = {
            ...mockBotState,
            health: {
                tradingControl: {
                    mode: 'PAUSED',
                    lastChangedBy: 'admin',
                    lastChangedAt: Date.now()
                }
            }
        };
        render(<AdminTab botState={pausedState} />);
        const pauseButton = screen.getByText(/Pause Auto Trading/i);
        expect(pauseButton).toBeDisabled();
    });

    test('should disable resume button when already running', () => {
        render(<AdminTab botState={mockBotState} />);
        const resumeButton = screen.getByText(/Resume Auto Trading/i);
        expect(resumeButton).toBeDisabled();
    });

    test('should show loading state when API call in progress', async () => {
        global.fetch = jest.fn(() => new Promise(() => {})); // Never resolves
        
        render(<AdminTab botState={mockBotState} />);
        const pauseButton = screen.getByText(/Pause Auto Trading/i);
        
        fireEvent.click(pauseButton);
        
        await waitFor(() => {
            expect(screen.getByText(/Pausing.../i)).toBeInTheDocument();
        });
    });

    test('should show error when API call fails', async () => {
        global.fetch = jest.fn(() => 
            Promise.resolve({
                ok: false,
                json: () => Promise.resolve({ error: 'Unauthorized' })
            })
        );
        
        render(<AdminTab botState={mockBotState} />);
        const pauseButton = screen.getByText(/Pause Auto Trading/i);
        
        fireEvent.click(pauseButton);
        
        await waitFor(() => {
            expect(screen.getByText(/Unauthorized/i)).toBeInTheDocument();
        });
    });

    test('should display safety notice', () => {
        render(<AdminTab botState={mockBotState} />);
        expect(screen.getByText(/Safety Note:/i)).toBeInTheDocument();
        expect(screen.getByText(/Existing positions will continue/i)).toBeInTheDocument();
    });
});
```

### App Header Tests

**File**: `bytelyst-trading-dashboard-web/src/App.test.tsx`

```typescript
describe('App - Trading Control Header Badge', () => {
    test('should show trading active badge when running', () => {
        const runningState = {
            health: {
                tradingControl: {
                    mode: 'RUNNING',
                    lastChangedBy: 'system',
                    lastChangedAt: Date.now()
                }
            }
        };
        render(<App botState={runningState} />);
        expect(screen.getByText(/Trading Active/i)).toBeInTheDocument();
    });

    test('should show trading paused badge when paused', () => {
        const pausedState = {
            health: {
                tradingControl: {
                    mode: 'PAUSED',
                    lastChangedBy: 'admin',
                    lastChangedAt: Date.now()
                }
            }
        };
        render(<App botState={pausedState} />);
        expect(screen.getByText(/Trading Paused/i)).toBeInTheDocument();
    });

    test('should show tooltip with pause details', () => {
        const pausedState = {
            health: {
                tradingControl: {
                    mode: 'PAUSED',
                    lastChangedBy: 'admin@test.com',
                    lastChangedAt: Date.now()
                }
            }
        };
        render(<App botState={pausedState} />);
        const badge = screen.getByText(/Trading Paused/i).closest('div');
        expect(badge).toHaveAttribute('title', expect.stringContaining('admin@test.com'));
    });
});
```

## API Tests

### Security Tests

```typescript
describe('Trading Control API - Security', () => {
    test('should reject pause request without auth token', async () => {
        await request(app)
            .post('/internal/trading/pause')
            .send({ reason: 'Test' })
            .expect(401);
    });

    test('should reject pause request from non-admin user', async () => {
        await request(app)
            .post('/internal/trading/pause')
            .set('Authorization', `Bearer ${regularUserToken}`)
            .send({ reason: 'Test' })
            .expect(403);
    });

    test('should allow pause request from admin user', async () => {
        await request(app)
            .post('/internal/trading/pause')
            .set('Authorization', `Bearer ${adminToken}`)
            .send({ reason: 'Test' })
            .expect(200);
    });

    test('should allow status check from any authenticated user', async () => {
        await request(app)
            .get('/internal/trading/status')
            .set('Authorization', `Bearer ${regularUserToken}`)
            .expect(200);
    });
});
```

### Idempotency Tests

```typescript
describe('Trading Control API - Idempotency', () => {
    test('should be idempotent when pausing already paused system', async () => {
        // Pause once
        const res1 = await request(app)
            .post('/internal/trading/pause')
            .set('Authorization', `Bearer ${adminToken}`)
            .send({ reason: 'Test' })
            .expect(200);

        // Pause again
        const res2 = await request(app)
            .post('/internal/trading/pause')
            .set('Authorization', `Bearer ${adminToken}`)
            .send({ reason: 'Test' })
            .expect(200);

        expect(res1.body.status.mode).toBe('PAUSED');
        expect(res2.body.status.mode).toBe('PAUSED');
    });

    test('should be idempotent when resuming already running system', async () => {
        // Resume once
        const res1 = await request(app)
            .post('/internal/trading/resume')
            .set('Authorization', `Bearer ${adminToken}`)
            .send({ reason: 'Test' })
            .expect(200);

        // Resume again
        const res2 = await request(app)
            .post('/internal/trading/resume')
            .set('Authorization', `Bearer ${adminToken}`)
            .send({ reason: 'Test' })
            .expect(200);

        expect(res1.body.status.mode).toBe('RUNNING');
        expect(res2.body.status.mode).toBe('RUNNING');
    });
});
```

## Manual Testing Checklist

### Backend Testing

- [ ] Start bot in RUNNING mode
- [ ] Call `/internal/trading/pause` as admin → verify success
- [ ] Attempt to place entry order → verify blocked
- [ ] Verify existing position still monitored
- [ ] Call `/internal/trading/resume` as admin → verify success
- [ ] Attempt to place entry order → verify allowed
- [ ] Restart bot → verify state persisted
- [ ] Call `/internal/trading/pause` as non-admin → verify 403
- [ ] Call `/internal/trading/status` → verify correct state returned

### Frontend Testing

- [ ] Login as admin user
- [ ] Navigate to Admin tab
- [ ] Verify Trading Control section visible
- [ ] Click "Pause Auto Trading" → verify button disabled, status updates
- [ ] Verify header badge shows "Trading Paused"
- [ ] Navigate to other tabs → verify header badge still visible
- [ ] Click "Resume Auto Trading" → verify button disabled, status updates
- [ ] Verify header badge shows "Trading Active"
- [ ] Simulate API error → verify error message displayed
- [ ] Login as non-admin user → verify Trading Control section hidden

### Edge Cases

- [ ] Pause while entry order is pending → verify order completes
- [ ] Pause while exit order is pending → verify order completes
- [ ] WebSocket disconnect while paused → verify status persists on reconnect
- [ ] Multiple admins pause/resume simultaneously → verify last write wins
- [ ] Bot restart while paused → verify still paused after restart

## Performance Testing

- [ ] Measure latency of pause/resume API calls
- [ ] Verify no performance impact on trading loop when paused
- [ ] Verify no performance impact on position monitoring when paused
- [ ] Test with 100+ concurrent pause/resume requests

## Acceptance Criteria

✅ **All tests pass**
✅ **No new entries when paused**
✅ **Existing positions continue lifecycle**
✅ **Only admins can pause/resume**
✅ **State persists across restarts**
✅ **UI accurately reflects backend state**
✅ **Errors handled gracefully**
✅ **Audit logs capture all changes**

## Test Execution

Run backend tests:
```bash
cd bytelyst-trading-bot-service
npm test -- --testPathPattern=healthTracker
npm test -- --testPathPattern=AutoTrader
npm test -- --testPathPattern=TradeExecutor
```

Run frontend tests:
```bash
cd bytelyst-trading-dashboard-web
npm test -- --testPathPattern=AdminTab
npm test -- --testPathPattern=App
```

Run integration tests:
```bash
cd bytelyst-trading-bot-service
npm run test:integration
```
