import { afterEach, describe, expect, it, vi } from 'vitest';
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
import { getRegistry } from '@jackwener/opencli/registry';
import { __test__ } from './utils.js';
import { __test__ as priceTest } from './price.js';
import { __test__ as trainTest } from './train.js';
import { __test__ as trainsTest } from './trains.js';
import './orders.js';

const { parseStationBundle, resolveStation, validateDate, buildCookieHeader, parseTrainRecord, maskEmail, maskMobile, maskChineseName, unwrapEvaluateResult, requireEvaluateObject, isAuthLikePayload } = __test__;
const { parsePriceData, queryStopsForPrice, queryPrice, TRAIN_NO_RE: PRICE_TRAIN_NO_RE } = priceTest;
const { queryStops, TRAIN_NO_RE: TRAIN_TRAIN_NO_RE } = trainTest;
const { queryLeftTickets, extractQueryEndpoint } = trainsTest;

afterEach(() => {
    vi.unstubAllGlobals();
});

describe('12306 utils - parseStationBundle', () => {
    it('parses the `@`-delimited station bundle into structured records', () => {
        const bundle = "var station_names ='@bjb|北京北|VAP|beijingbei|bjb|0|0357|北京|||@bji|北京|BJP|beijing|bj|2|0357|北京|||@aoh|上海虹桥|AOH|shanghaihongqiao|shhq|10|7600|上海|||';";
        const stations = parseStationBundle(bundle);
        expect(stations).toHaveLength(3);
        expect(stations[1]).toEqual({
            short: 'bji', name: '北京', code: 'BJP', pinyin: 'beijing', abbr: 'bj', city: '北京',
        });
    });

    it('skips records that lack a telecode', () => {
        const bundle = "var station_names ='@xxx|||||||||@bji|北京|BJP|beijing|bj|2|0357|北京|||';";
        const stations = parseStationBundle(bundle);
        expect(stations).toHaveLength(1);
        expect(stations[0].code).toBe('BJP');
    });

    it('throws CommandExecutionError when the bundle has no parseable station rows', () => {
        expect(() => parseStationBundle("var station_names ='@xxx|||||||||';")).toThrow(CommandExecutionError);
    });
});

describe('12306 utils - resolveStation', () => {
    const stations = [
        { short: 'bjb', name: '北京北', code: 'VAP', pinyin: 'beijingbei', abbr: 'bjb', city: '北京' },
        { short: 'bji', name: '北京', code: 'BJP', pinyin: 'beijing', abbr: 'bj', city: '北京' },
        { short: 'aoh', name: '上海虹桥', code: 'AOH', pinyin: 'shanghaihongqiao', abbr: 'shhq', city: '上海' },
    ];

    it('matches by exact Chinese name', () => {
        expect(resolveStation(stations, '上海虹桥').code).toBe('AOH');
    });

    it('matches by uppercase telecode', () => {
        expect(resolveStation(stations, 'BJP').code).toBe('BJP');
    });

    it('matches by full pinyin (case-insensitive)', () => {
        expect(resolveStation(stations, 'Beijing').code).toBe('BJP');
    });

    it('matches by short alias / abbr', () => {
        expect(resolveStation(stations, 'shhq').code).toBe('AOH');
    });

    it('throws ArgumentError for empty input', () => {
        expect(() => resolveStation(stations, '   ')).toThrow(ArgumentError);
    });

    it('throws ArgumentError for unknown station', () => {
        expect(() => resolveStation(stations, '某不存在站')).toThrow(ArgumentError);
    });

    it('throws ArgumentError for telecode-shaped but unknown input', () => {
        expect(() => resolveStation(stations, 'XYZ')).toThrow(ArgumentError);
    });
});

describe('12306 utils - validateDate', () => {
    it('accepts valid YYYY-MM-DD', () => {
        expect(validateDate('2026-05-22')).toBe('2026-05-22');
    });

    it('throws ArgumentError on wrong format', () => {
        expect(() => validateDate('2026/05/22')).toThrow(ArgumentError);
        expect(() => validateDate('26-05-22')).toThrow(ArgumentError);
        expect(() => validateDate('today')).toThrow(ArgumentError);
        expect(() => validateDate('')).toThrow(ArgumentError);
    });

    it('throws ArgumentError on impossible calendar dates', () => {
        expect(() => validateDate('2026-02-30')).toThrow(ArgumentError);
        expect(() => validateDate('2026-13-01')).toThrow(ArgumentError);
    });
});

describe('12306 utils - buildCookieHeader', () => {
    it('joins set-cookie lines into a single Cookie header', () => {
        const headers = [
            'JSESSIONID=ABC123; Path=/otn',
            'BIGipServerotn=xxx.yyy; Path=/',
            'route=zzz; Expires=Sat, 01 Jan 2027 00:00:00 GMT',
        ];
        expect(buildCookieHeader(headers)).toBe('JSESSIONID=ABC123; BIGipServerotn=xxx.yyy; route=zzz');
    });

    it('returns empty string for empty input', () => {
        expect(buildCookieHeader([])).toBe('');
        expect(buildCookieHeader(undefined)).toBe('');
    });
});

describe('12306 utils - parseTrainRecord', () => {
    const stationByCode = new Map([
        ['VNP', { name: '北京南', code: 'VNP' }],
        ['AOH', { name: '上海虹桥', code: 'AOH' }],
    ]);

    it('extracts the canonical train fields from a wire record', () => {
        // 33 `|`-separated fields, with positions used by parseTrainRecord populated.
        const fields = new Array(36).fill('');
        fields[0] = 'SECRET_TOKEN';
        fields[1] = '预订';
        fields[2] = '240000G54700';
        fields[3] = 'G547';
        fields[6] = 'VNP';
        fields[7] = 'AOH';
        fields[8] = '06:18';
        fields[9] = '12:11';
        fields[10] = '05:53';
        fields[11] = 'Y';
        fields[23] = ''; // soft sleeper
        fields[26] = '无'; // no seat
        fields[28] = ''; // hard sleeper
        fields[29] = ''; // hard seat
        fields[30] = '有'; // second seat
        fields[31] = '有'; // first seat
        fields[32] = '无'; // business seat
        const row = parseTrainRecord(fields.join('|'), stationByCode);
        expect(row).toEqual({
            train_no: '240000G54700',
            code: 'G547',
            from_station: '北京南',
            to_station: '上海虹桥',
            from_code: 'VNP',
            to_code: 'AOH',
            start_time: '06:18',
            arrive_time: '12:11',
            duration: '05:53',
            available: true,
            business_seat: '无',
            first_seat: '有',
            second_seat: '有',
            soft_sleeper: '',
            hard_sleeper: '',
            hard_seat: '',
            no_seat: '无',
        });
    });

    it('does not expose the booking-handshake secret token', () => {
        const fields = new Array(36).fill('');
        fields[0] = 'SECRET_TOKEN_DO_NOT_LEAK';
        fields[2] = 't_no'; fields[3] = 'X1'; fields[6] = 'VNP'; fields[7] = 'AOH';
        const row = parseTrainRecord(fields.join('|'), stationByCode);
        expect(Object.values(row)).not.toContain('SECRET_TOKEN_DO_NOT_LEAK');
        expect('secret' in row).toBe(false);
    });

    it('falls back to the telecode when the station bundle has no name', () => {
        const fields = new Array(36).fill('');
        fields[2] = 'X'; fields[3] = 'X'; fields[6] = 'ZZZ'; fields[7] = 'YYY';
        const row = parseTrainRecord(fields.join('|'), stationByCode);
        expect(row.from_station).toBe('ZZZ');
        expect(row.to_station).toBe('YYY');
    });

    it('returns null for short records', () => {
        expect(parseTrainRecord('a|b|c', stationByCode)).toBeNull();
    });
});

describe('12306 utils - mask helpers', () => {
    it('masks the local-part of an email', () => {
        expect(maskEmail('hello@example.com')).toBe('h***o@example.com');
        expect(maskEmail('ab@x.cn')).toBe('a*@x.cn');
        expect(maskEmail('a@x.cn')).toBe('a*@x.cn');
        expect(maskEmail('')).toBe('');
        expect(maskEmail('not-an-email')).toBe('not-an-email');
    });

    it('masks Chinese mobile numbers while preserving 12306-side masks', () => {
        expect(maskMobile('13800001234')).toBe('138****1234');
        expect(maskMobile('138****1234')).toBe('138****1234');
        expect(maskMobile('')).toBe('');
        expect(maskMobile('123')).toBe('**3');
    });

    it('masks Chinese real names', () => {
        expect(maskChineseName('张三')).toBe('张*');
        expect(maskChineseName('李四明')).toBe('李*明');
        expect(maskChineseName('欧阳锋')).toBe('欧*锋');
        expect(maskChineseName('张')).toBe('张');
        expect(maskChineseName('')).toBe('');
    });
});

describe('12306 price - parsePriceData', () => {
    it('returns seat rows sorted by descending price and drops dup numeric codes', () => {
        const data = {
            train_no: '24000000G10L',
            'OT': [],
            'A9': '¥2158.0',
            '9': '21580',
            'P': '¥1163.0',
            'M': '¥1035.0',
            'O': '¥626.0',
            'WZ': '¥626.0',
            'INVALID': 'not-a-price',
        };
        const rows = parsePriceData(data);
        const codes = rows.map((r) => r.seat_code);
        expect(codes).not.toContain('9');
        expect(codes).not.toContain('OT');
        expect(codes).not.toContain('train_no');
        expect(codes).not.toContain('INVALID');
        expect(codes).toEqual(['A9', 'P', 'M', 'O', 'WZ']);
        expect(rows[0]).toEqual({ seat_code: 'A9', seat_name: '商务座', price: '2158.0', currency: 'CNY' });
        expect(rows[4]).toEqual({ seat_code: 'WZ', seat_name: '无座', price: '626.0', currency: 'CNY' });
    });

    it('keeps unknown letter codes with the letter as the name', () => {
        const data = { 'A9': '¥100.0', 'ZZ': '¥50.0' };
        const rows = parsePriceData(data);
        const zz = rows.find((r) => r.seat_code === 'ZZ');
        expect(zz?.seat_name).toBe('ZZ');
    });
});

describe('12306 train_no validation regex', () => {
    // 12306 train_no values returned by /otn/leftTicket/query sometimes contain
    // lowercase letters (e.g. "5l000G1970A3" for G1970 上海虹桥 -> 宝鸡南).
    // Both `12306 price` and `12306 train` must accept the raw value emitted
    // by `12306 trains`, otherwise the two adapters drift apart and downstream
    // calls fail with ARGUMENT before ever hitting 12306.
    for (const [label, re] of [['price', PRICE_TRAIN_NO_RE], ['train', TRAIN_TRAIN_NO_RE]]) {
        describe(label, () => {
            it('accepts an all-uppercase train_no', () => {
                expect(re.test('24000000G10L')).toBe(true);
            });
            it('accepts a train_no with lowercase letters (real 12306 payload)', () => {
                expect(re.test('5l000G1970A3')).toBe(true);
            });
            it('rejects public codes like G1970', () => {
                expect(re.test('G1970')).toBe(false);
            });
            it('rejects values with disallowed characters', () => {
                expect(re.test('5l000-G1970A3')).toBe(false);
            });
        });
    }
});

describe('12306 public API typed boundaries', () => {
    const nonJsonFetch = async () => ({
        ok: true,
        json: async () => {
            throw new SyntaxError('Unexpected token <');
        },
    });

    it('wraps non-JSON train stop bodies as CommandExecutionError', async () => {
        await expect(queryStops('cookie=1', '24000000G10L', 'BJP', 'AOH', '2026-05-22', nonJsonFetch))
            .rejects.toBeInstanceOf(CommandExecutionError);
    });

    it('wraps non-JSON price helper bodies as CommandExecutionError', async () => {
        await expect(queryStopsForPrice('cookie=1', '24000000G10L', 'BJP', 'AOH', '2026-05-22', nonJsonFetch))
            .rejects.toBeInstanceOf(CommandExecutionError);
        await expect(queryPrice('cookie=1', '24000000G10L', '01', '02', 'OM9', '2026-05-22', nonJsonFetch))
            .rejects.toBeInstanceOf(CommandExecutionError);
    });
});

describe('12306 trains endpoint rotation', () => {
    const successBody = { data: { result: ['row|payload'] } };

    it('extracts only leftTicket query endpoints from rotation hints', () => {
        expect(extractQueryEndpoint('leftTicket/queryB')).toBe('queryB');
        expect(extractQueryEndpoint('/otn/leftTicket/queryC')).toBe('queryC');
        expect(extractQueryEndpoint('https://kyfw.12306.cn/otn/leftTicket/queryD')).toBe('queryD');
        expect(extractQueryEndpoint('/otn/error.html')).toBe('');
        expect(extractQueryEndpoint('https://example.com/leftTicket/queryB')).toBe('');
        expect(extractQueryEndpoint('leftTicket/querybad')).toBe('');
    });

    it('follows a 302 JSON c_url rotation signal before trying fallback endpoints', async () => {
        const fetchMock = vi.fn()
            .mockResolvedValueOnce(new Response(JSON.stringify({ c_url: 'leftTicket/queryB' }), { status: 302 }))
            .mockResolvedValueOnce(new Response(JSON.stringify(successBody), { status: 200 }));
        vi.stubGlobal('fetch', fetchMock);

        await expect(queryLeftTickets('cookie=1', 'BJP', 'AOH', '2026-05-22')).resolves.toEqual(['row|payload']);
        expect(fetchMock).toHaveBeenCalledTimes(2);
        expect(fetchMock.mock.calls[0][0]).toContain('/leftTicket/queryG?');
        expect(fetchMock.mock.calls[1][0]).toContain('/leftTicket/queryB?');
    });

    it('follows a 302 Location header rotation signal when the body is not JSON', async () => {
        const fetchMock = vi.fn()
            .mockResolvedValueOnce(new Response('<html>redirect</html>', {
                status: 302,
                headers: { location: '/otn/leftTicket/queryB' },
            }))
            .mockResolvedValueOnce(new Response(JSON.stringify(successBody), { status: 200 }));
        vi.stubGlobal('fetch', fetchMock);

        await expect(queryLeftTickets('cookie=1', 'BJP', 'AOH', '2026-05-22')).resolves.toEqual(['row|payload']);
        expect(fetchMock).toHaveBeenCalledTimes(2);
        expect(fetchMock.mock.calls[1][0]).toContain('/leftTicket/queryB?');
    });

    it('typed-fails a 302 that does not identify a leftTicket query endpoint', async () => {
        const fetchMock = vi.fn().mockResolvedValueOnce(new Response('<html>error</html>', {
            status: 302,
            headers: { location: '/otn/error.html' },
        }));
        vi.stubGlobal('fetch', fetchMock);

        await expect(queryLeftTickets('cookie=1', 'BJP', 'AOH', '2026-05-22'))
            .rejects.toBeInstanceOf(CommandExecutionError);
        expect(fetchMock).toHaveBeenCalledTimes(1);
    });

    it('deduplicates rotation endpoints request-locally and keeps fallback bounded', async () => {
        const fetchMock = vi.fn()
            .mockResolvedValueOnce(new Response(JSON.stringify({ c_url: 'leftTicket/queryG' }), { status: 302 }))
            .mockResolvedValueOnce(new Response(JSON.stringify(successBody), { status: 200 }));
        vi.stubGlobal('fetch', fetchMock);

        await expect(queryLeftTickets('cookie=1', 'BJP', 'AOH', '2026-05-22')).resolves.toEqual(['row|payload']);
        expect(fetchMock).toHaveBeenCalledTimes(2);
        expect(fetchMock.mock.calls[0][0]).toContain('/leftTicket/queryG?');
        expect(fetchMock.mock.calls[1][0]).toContain('/leftTicket/queryO?');
    });
});

describe('12306 browser evaluate boundaries', () => {
    it('unwraps Browser Bridge {session,data} evaluate envelopes only at the boundary', () => {
        expect(unwrapEvaluateResult({ session: 's1', data: 'JSESSIONID=1; tk=2' })).toBe('JSESSIONID=1; tk=2');
        expect(unwrapEvaluateResult({ status: true, data: { value: 1 } })).toEqual({ status: true, data: { value: 1 } });
        expect(requireEvaluateObject({ session: 's1', data: { status: true } }, 'test')).toEqual({ status: true });
        expect(() => requireEvaluateObject({ session: 's1', data: null }, 'test')).toThrow(CommandExecutionError);
    });

    it('classifies 12306 login-like API envelopes as auth failures', () => {
        expect(isAuthLikePayload({ status: false, messages: ['用户未登录'] })).toBe(true);
        expect(isAuthLikePayload({ status: false, validateMessages: { global: ['请登录后再试'] } })).toBe(true);
        expect(isAuthLikePayload({ status: false, messages: ['系统繁忙'] })).toBe(false);
    });

    it('masks passenger names in orders by default and supports explicit sensitive opt-in', async () => {
        const command = getRegistry().get('12306/orders');
        const makePage = () => ({
            goto: async () => {},
            evaluate: async (script) => {
                if (script === "document.cookie || ''") return { session: 'browser', data: 'JSESSIONID=abc; tk=def' };
                return {
                    session: 'browser',
                    data: {
                        status: true,
                        data: {
                            orderDBList: [{
                                sequence_no: 'E123',
                                order_date: '2026-05-18 10:00',
                                train_code_page: 'G1',
                                from_station_name_page: '北京南',
                                to_station_name_page: '上海虹桥',
                                start_train_date_page: '2026-05-22 07:00',
                                ticket_status_name: '未出行',
                                ticket_total_price_page: '626.0',
                                tickets: [{ passenger_name: '张三' }, { passenger_name: '李四明' }],
                            }],
                        },
                    },
                };
            },
        });

        await expect(command.func(makePage(), {})).resolves.toMatchObject([
            { order_id: 'E123', passengers: '张*, 李*明' },
        ]);
        await expect(command.func(makePage(), { 'include-sensitive': true })).resolves.toMatchObject([
            { order_id: 'E123', passengers: '张三, 李四明' },
        ]);
    });

    it('maps login-like order payloads to AuthRequiredError instead of parser drift', async () => {
        const command = getRegistry().get('12306/orders');
        const page = {
            goto: async () => {},
            evaluate: async (script) => {
                if (script === "document.cookie || ''") return 'JSESSIONID=abc; tk=def';
                return { status: false, messages: ['用户未登录'] };
            },
        };

        await expect(command.func(page, {})).rejects.toBeInstanceOf(AuthRequiredError);
    });

    it('treats missing order list shape as parser drift but known empty arrays as empty result', async () => {
        const command = getRegistry().get('12306/orders');
        const makePage = (payload) => ({
            goto: async () => {},
            evaluate: async (script) => {
                if (script === "document.cookie || ''") return 'JSESSIONID=abc; tk=def';
                return payload;
            },
        });

        await expect(command.func(makePage({ status: true, data: {} }), {}))
            .rejects.toBeInstanceOf(CommandExecutionError);
        await expect(command.func(makePage({ status: true, data: { orderDBList: [] } }), {}))
            .rejects.toBeInstanceOf(EmptyResultError);
    });
});
