import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Bot } from './bot';
import type { Alpaca } from './alpaca';
import type { Strategy, Signal } from './strategy';

function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
    return {
        getAccount: vi.fn().mockResolvedValue({ cash: '10000.00' }),
        getAssets: vi.fn(),
        getAsset: vi.fn(),
        getClock: vi.fn().mockResolvedValue({
            is_open: false,
            next_open: new Date().toISOString(),
            next_close: new Date().toISOString(),
        }),
        getLatestQuote: vi.fn(),
        getLatestTrades: vi.fn(),
        ...overrides,
    } as unknown as Alpaca;
}

function mockStrategy(overrides: Partial<Strategy> = {}): Strategy {
    return {
        name: 'test-strategy',
        capitalAllocation: 0.5,
        execute: vi.fn().mockResolvedValue([]),
        ...overrides,
    };
}

beforeEach(() => {
    vi.useFakeTimers();
    vi.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
    vi.useRealTimers();
    vi.restoreAllMocks();
});

describe('Bot', () => {
    describe('constructor', () => {
        it('throws when capital allocations exceed 1.0', () => {
            const alpaca = mockAlpaca();
            const strategies = [
                mockStrategy({ capitalAllocation: 0.6 }),
                mockStrategy({ capitalAllocation: 0.5 }),
            ];
            expect(() => new Bot(alpaca, strategies)).toThrow(
                'Capital allocations sum to 1.1, which exceeds 1.0'
            );
        });

        it('accepts allocations that sum to exactly 1.0', () => {
            const alpaca = mockAlpaca();
            const strategies = [
                mockStrategy({ capitalAllocation: 0.5 }),
                mockStrategy({ capitalAllocation: 0.5 }),
            ];
            expect(() => new Bot(alpaca, strategies)).not.toThrow();
        });

        it('accepts allocations that sum to less than 1.0', () => {
            const alpaca = mockAlpaca();
            const strategies = [
                mockStrategy({ capitalAllocation: 0.3 }),
                mockStrategy({ capitalAllocation: 0.2 }),
            ];
            expect(() => new Bot(alpaca, strategies)).not.toThrow();
        });
    });

    describe('runDay', () => {
        it('runs multiple strategies concurrently', async () => {
            const alpaca = mockAlpaca();
            const strategyA = mockStrategy({
                name: 'A',
                capitalAllocation: 0.3,
                execute: vi.fn().mockResolvedValue([
                    { symbol: 'TQQQ', direction: 'buy', allocation: 1.0 },
                ]),
            });
            const strategyB = mockStrategy({
                name: 'B',
                capitalAllocation: 0.2,
                execute: vi.fn().mockResolvedValue([
                    { symbol: 'SPY', direction: 'sell', allocation: 0.5 },
                ]),
            });

            const bot = new Bot(alpaca, [strategyA, strategyB]);
            const promise = bot.runDay();
            await vi.advanceTimersByTimeAsync(0);
            await promise;

            expect(strategyA.execute).toHaveBeenCalledWith(alpaca);
            expect(strategyB.execute).toHaveBeenCalledWith(alpaca);
            // A: 10000 * 0.3 * 1.0 = 3000
            expect(console.log).toHaveBeenCalledWith('[A] BUY TQQQ — $3000.00');
            // B: 10000 * 0.2 * 0.5 = 1000
            expect(console.log).toHaveBeenCalledWith('[B] SELL SPY — $1000.00');
        });

        it('passes signals to executor', async () => {
            const alpaca = mockAlpaca();
            const signals: Signal[] = [
                { symbol: 'TQQQ', direction: 'buy', allocation: 0.5 },
            ];
            const strategy = mockStrategy({
                name: 'test',
                capitalAllocation: 0.4,
                execute: vi.fn().mockResolvedValue(signals),
            });

            const bot = new Bot(alpaca, [strategy]);
            const promise = bot.runDay();
            await vi.advanceTimersByTimeAsync(0);
            await promise;

            // 10000 * 0.4 * 0.5 = 2000
            expect(console.log).toHaveBeenCalledWith('[test] BUY TQQQ — $2000.00');
        });

        it('works with zero strategies', async () => {
            const alpaca = mockAlpaca();
            const bot = new Bot(alpaca, []);

            const promise = bot.runDay();
            await vi.advanceTimersByTimeAsync(0);
            await promise;

            // Only the "waiting for open" log, no strategy logs
            expect(console.log).toHaveBeenCalledTimes(1);
            expect(console.log).toHaveBeenCalledWith('waiting for open');
        });
    });
});
