import { flushPromises, mount } from '@vue/test-utils';
import { describe, expect, it, vi } from 'vitest';
import { KviewIcon } from '@kview/ui-core/vue';
import {
    KviewChart,
    KviewInfoCard,
    KviewMetricTrend,
    KviewPerformanceBadge,
    KviewPerformanceChart,
} from '@kview/visualization/vue';

const chartInstances: Array<{
    config: { type: string; data: unknown; options: unknown };
    destroy: ReturnType<typeof vi.fn>;
    update: ReturnType<typeof vi.fn>;
}> = [];

vi.mock('chart.js', () => {
    class ChartStub {
        static register = vi.fn();

        config: { type: string; data: unknown; options: unknown };

        destroy = vi.fn();

        update = vi.fn();

        constructor(_canvas: unknown, config: { type: string; data: unknown; options: unknown }) {
            this.config = config;
            chartInstances.push(this as never);
        }
    }

    return {
        Chart: ChartStub,
        BarController: {},
        BarElement: {},
        CategoryScale: {},
        Legend: {},
        LinearScale: {},
        LineController: {},
        LineElement: {},
        PointElement: {},
        Tooltip: {},
    };
});

describe('KviewMetricTrend', () => {
    it('matches the Blade direction, automatic tone, icon, value and compact contract', async () => {
        const wrapper = mount(KviewMetricTrend, {
            props: {
                direction: 'up',
                value: 0,
                label: 'since inception',
                compact: true,
            },
        });

        expect(wrapper.classes()).toEqual(expect.arrayContaining([
            'ui-metric-trend',
            'ui-metric-trend--positive',
            'ui-metric-trend--compact',
        ]));
        expect(wrapper.get('strong').text()).toBe('0');
        expect(wrapper.get('.ui-metric-trend > span').text()).toBe('since inception');
        expect(wrapper.getComponent(KviewIcon).props()).toMatchObject({
            name: 'trend-up',
            size: 13,
        });

        await wrapper.setProps({
            direction: 'down',
            compact: false,
        });

        expect(wrapper.classes()).toContain('ui-metric-trend--negative');
        expect(wrapper.classes()).not.toContain('ui-metric-trend--compact');
        expect(wrapper.getComponent(KviewIcon).props('name')).toBe('trend-down');
    });

    it('supports explicit presentation, slots, attributes and safe runtime fallbacks', () => {
        const explicit = mount(KviewMetricTrend, {
            props: {
                direction: 'down',
                value: '-2.5%',
                label: 'quarter',
                tone: 'positive',
                icon: 'investment',
            },
            attrs: {
                class: 'custom-trend',
                'data-contract': 'visualization',
            },
            slots: {
                default: '<em class="trend-suffix">audited</em>',
            },
        });

        expect(explicit.classes()).toEqual(expect.arrayContaining([
            'ui-metric-trend--positive',
            'custom-trend',
        ]));
        expect(explicit.attributes('data-contract')).toBe('visualization');
        expect(explicit.getComponent(KviewIcon).props('name')).toBe('investment');
        expect(explicit.get('.trend-suffix').text()).toBe('audited');

        const fallback = mount(KviewMetricTrend, {
            props: {
                direction: 'up',
                tone: '0' as never,
                icon: '0',
            },
        });

        expect(fallback.classes()).toContain('ui-metric-trend--positive');
        expect(fallback.getComponent(KviewIcon).props('name')).toBe('trend-up');
        expect(fallback.html()).not.toContain('data-ui-');

        const invalid = mount(KviewMetricTrend, {
            props: {
                direction: 'sideways' as never,
                tone: 'warning' as never,
                icon: '',
            },
        });

        expect(invalid.classes()).toContain('ui-metric-trend--neutral');
        expect(invalid.getComponent(KviewIcon).props('name')).toBe('minus');
    });
});

describe('KviewInfoCard', () => {
    it('matches the Blade metric card composition with icon, meta and nested trend', () => {
        const wrapper = mount(KviewInfoCard, {
            props: {
                label: 'Net asset value',
                value: '38.2M €',
                icon: 'wallet',
                variant: 'metric',
                meta: 'Latest reported NAV',
                trendValue: '+4.2%',
                trendLabel: 'year to date',
                trendDirection: 'up',
            },
            attrs: {
                class: 'nav-card',
                'data-contract': 'visualization',
            },
        });

        expect(wrapper.classes()).toEqual(expect.arrayContaining([
            'info-card',
            'info-card--metric',
            'nav-card',
        ]));
        expect(wrapper.attributes('data-contract')).toBe('visualization');
        expect(wrapper.get('.info-card-label').text()).toBe('Net asset value');
        expect(wrapper.get('.info-card-value').text()).toBe('38.2M €');
        expect(wrapper.get('.info-card-meta').text()).toBe('Latest reported NAV');
        expect(wrapper.get('.info-card-icon').attributes('aria-hidden')).toBe('true');
        expect(wrapper.getComponent(KviewIcon).props()).toMatchObject({
            name: 'wallet',
            size: 16,
        });

        const trend = wrapper.getComponent(KviewMetricTrend);

        expect(trend.classes()).toContain('info-card-trend');
        expect(trend.props()).toMatchObject({
            direction: 'up',
            value: '+4.2%',
            label: 'year to date',
            compact: true,
        });
        expect(trend.classes()).toContain('ui-metric-trend--positive');
    });

    it('uses the slot fallback, keeps numeric zero, and normalizes unsafe runtime props', () => {
        const slotted = mount(KviewInfoCard, {
            props: {
                label: 'From',
                value: null,
            },
            slots: {
                default: '<span class="currency-slot">EUR</span>',
            },
        });

        expect(slotted.get('.info-card-value').text()).toBe('EUR');
        expect(slotted.find('.info-card-icon').exists()).toBe(false);
        expect(slotted.find('.info-card-meta').exists()).toBe(false);
        expect(slotted.findComponent(KviewMetricTrend).exists()).toBe(false);

        const fallback = mount(KviewInfoCard, {
            props: {
                label: 'Rate',
                value: 0,
                icon: '0',
                variant: 'panel' as never,
                meta: 0,
                trendValue: 0,
                trendDirection: 'sideways' as never,
            },
            slots: {
                default: 'must not replace zero',
            },
        });

        expect(fallback.classes()).not.toContain('info-card--metric');
        expect(fallback.get('.info-card-value').text()).toBe('0');
        expect(fallback.find('.info-card-icon').exists()).toBe(false);
        expect(fallback.find('.info-card-meta').exists()).toBe(false);
        expect(fallback.getComponent(KviewMetricTrend).props()).toMatchObject({
            direction: 'neutral',
            value: 0,
            compact: false,
        });
        expect(fallback.getComponent(KviewMetricTrend).classes()).toContain('ui-metric-trend--neutral');
        expect(fallback.html()).not.toContain('data-partial-placeholder');
        expect(fallback.html()).not.toContain('data-ui-');
    });
});

describe('KviewPerformanceBadge', () => {
    it('matches the Blade automatic tone, icon, value and shape contract', async () => {
        const wrapper = mount(KviewPerformanceBadge, {
            props: {
                value: '+4.2',
            },
        });

        expect(wrapper.classes()).toEqual(expect.arrayContaining([
            'ui-performance-badge',
            'ui-performance-badge--positive',
            'ui-performance-badge--badge',
        ]));
        expect(wrapper.get('strong').text()).toBe('+4.2');
        expect(wrapper.getComponent(KviewIcon).props()).toMatchObject({
            name: 'trend-up',
            size: 12,
        });

        await wrapper.setProps({ value: '−0.0' });

        expect(wrapper.classes()).toContain('ui-performance-badge--negative');
        expect(wrapper.getComponent(KviewIcon).props('name')).toBe('trend-down');

        await wrapper.setProps({ value: 0 });

        expect(wrapper.classes()).toContain('ui-performance-badge--neutral');
        expect(wrapper.get('strong').text()).toBe('0');
        expect(wrapper.getComponent(KviewIcon).props('name')).toBe('minus');
    });

    it('supports explicit presentation, dot priority, slots and safe runtime fallbacks', () => {
        const explicit = mount(KviewPerformanceBadge, {
            props: {
                label: 'Ajouté',
                tone: 'primary',
                icon: 'none',
                shape: 'inline',
            },
            attrs: {
                class: 'activity-status',
                'data-contract': 'visualization',
            },
            slots: {
                default: '<span class="badge-suffix">confirmé</span>',
            },
        });

        expect(explicit.classes()).toEqual(expect.arrayContaining([
            'ui-performance-badge--primary',
            'ui-performance-badge--inline',
            'activity-status',
        ]));
        expect(explicit.attributes('data-contract')).toBe('visualization');
        expect(explicit.findComponent(KviewIcon).exists()).toBe(false);
        expect(explicit.text()).toContain('Ajouté');
        expect(explicit.text()).toContain('confirmé');
        expect(explicit.html()).not.toContain('data-ui-');

        const dotted = mount(KviewPerformanceBadge, {
            props: {
                value: 12,
                dot: true,
            },
        });

        expect(dotted.classes()).toContain('ui-performance-badge--dot');
        expect(dotted.get('.ui-performance-badge-dot').attributes('aria-hidden')).toBe('true');
        expect(dotted.findComponent(KviewIcon).exists()).toBe(false);

        const fallback = mount(KviewPerformanceBadge, {
            props: {
                tone: 'unsupported' as never,
                shape: 'panel' as never,
            },
        });

        expect(fallback.classes()).toEqual(expect.arrayContaining([
            'ui-performance-badge--neutral',
            'ui-performance-badge--badge',
        ]));
    });
});

describe('KviewChart', () => {
    it('lazy-loads Chart.js and builds a combo config with dual axes and formatted tooltips', async () => {
        chartInstances.length = 0;

        const wrapper = mount(KviewChart, {
            props: {
                kind: 'bar',
                labels: ['Q1', 'Q2'],
                datasets: [
                    {
                        id: 'drawdowns', label: 'Drawdowns', values: [10, 20], valueFormat: 'amount',
                    },
                    {
                        id: 'tvpi', label: 'TVPI', values: [1.1, 1.4], kind: 'line', axis: 'secondary', style: 'dashed', valueFormat: 'ratio', emphasize: true,
                    },
                ],
                title: 'Quarterly cashflows',
                subtitle: 'Drawdowns vs. TVPI',
                summary: [{ id: 'net', label: 'Net', value: '12.4M €' }],
                primaryAxisLabel: 'Amount',
                secondaryAxisLabel: 'Multiple',
                ariaLabel: 'Quarterly drawdowns and TVPI chart',
            },
        });

        await flushPromises();

        expect(wrapper.find('.kview-chart--panel').exists()).toBe(true);
        expect(wrapper.get('.kview-chart__title').text()).toBe('Quarterly cashflows');
        expect(wrapper.get('.kview-chart__subtitle').text()).toBe('Drawdowns vs. TVPI');
        expect(wrapper.get('.kview-chart__metric-value').text()).toBe('12.4M €');

        const legendItems = wrapper.findAll('.kview-chart__legend-item');

        expect(legendItems).toHaveLength(2);
        expect(legendItems[1].find('.kview-chart__legend-line--dashed').exists()).toBe(true);

        expect(chartInstances).toHaveLength(1);
        const [instance] = chartInstances;

        expect(instance.config.type).toBe('bar');

        const { datasets } = instance.config.data as { datasets: Array<{ type: string; yAxisID: string }> };

        expect(datasets[0]).toMatchObject({ type: 'bar', yAxisID: 'y' });
        expect(datasets[1]).toMatchObject({ type: 'line', yAxisID: 'y1' });

        const { scales, plugins } = instance.config.options as {
            scales: { y1?: unknown };
            plugins: { tooltip: { callbacks: { label: (ctx: { dataset: { label: string }; parsed: { y: number } }) => string } } };
        };

        expect(scales.y1).toBeDefined();
        expect(plugins.tooltip.callbacks.label({ dataset: { label: 'TVPI' }, parsed: { y: 1.4 } })).toBe('TVPI: 1.40x');

        const srRows = wrapper.findAll('.kview-chart__sr-table tbody tr');

        expect(srRows).toHaveLength(2);
        expect(srRows[0].text()).toContain('Q1');
        expect(srRows[0].text()).toContain('10');
        expect(srRows[0].text()).toContain('1.10x');

        await wrapper.setProps({ labels: [], datasets: [] });
        await flushPromises();

        expect(instance.destroy).toHaveBeenCalled();
        expect(wrapper.find('.kview-chart__canvas').exists()).toBe(false);
        expect(wrapper.text()).toContain('No chart data');
    });

    it('shows the loading state before data and falls back to safe defaults for invalid kind and variant', async () => {
        const loading = mount(KviewChart, {
            props: {
                labels: [],
                datasets: [],
                loading: true,
                ariaLabel: 'Loading chart',
            },
        });

        expect(loading.text()).toContain('Loading chart');
        expect(loading.find('canvas').exists()).toBe(false);

        chartInstances.length = 0;

        const fallback = mount(KviewChart, {
            props: {
                kind: 'pie' as never,
                variant: 'panorama' as never,
                labels: ['Q1'],
                datasets: [{ id: 'a', label: 'A', values: [5] }],
                ariaLabel: 'Fallback chart',
            },
        });

        await flushPromises();

        expect(fallback.classes()).toContain('kview-chart--panel');
        expect(chartInstances[0].config.type).toBe('line');
    });
});

describe('KviewPerformanceChart', () => {
    const points = [
        { label: 'Q1 24', nav: 100, benchmark: 80 },
        { label: 'Q2 24', nav: 120, benchmark: 90 },
        { label: 'Q3 24', nav: 90, benchmark: 95 },
    ];

    it('plots NAV vs. benchmark with no external library, a legend, summary and accessible data table', async () => {
        const wrapper = mount(KviewPerformanceChart, {
            props: {
                points,
                title: 'Portfolio performance',
                navLabel: 'NAV',
                benchmarkLabel: 'Paid-in',
            },
        });

        expect(wrapper.find('canvas').exists()).toBe(false);
        expect(wrapper.get('.fund-performance-chart__title').text()).toBe('Portfolio performance');

        const legendItems = wrapper.findAll('.fund-performance-chart__legend-item');

        expect(legendItems).toHaveLength(2);
        expect(legendItems[0].text()).toBe('NAV');
        expect(legendItems[1].text()).toBe('Paid-in');

        expect(wrapper.get('.fund-performance-chart__metric-label').text()).toBe('Latest NAV');
        expect(wrapper.get('.fund-performance-chart__metric-value').text()).toBe('90.0');

        const trend = wrapper.get('.fund-performance-chart__trend');

        expect(trend.classes()).toContain('fund-performance-chart__trend--down');
        expect(trend.text()).toContain('-10.0%');

        const navPolyline = wrapper.get('.fund-performance-chart__line--nav');

        expect(navPolyline.attributes('points')?.split(' ')).toHaveLength(3);

        const rows = wrapper.findAll('.ui-visually-hidden tbody tr');

        expect(rows).toHaveLength(3);
        expect(rows[0].text()).toContain('Q1 24');
        expect(rows[0].text()).toContain('100.0');
        expect(rows[0].text()).toContain('80.0');

        const dataPoints = wrapper.findAll('.fund-performance-chart__data-point');

        expect(dataPoints.length).toBe(6);
        expect(wrapper.findAll('.fund-performance-chart__tooltip.is-tooltip-active')).toHaveLength(0);

        const lastNavPoint = dataPoints[dataPoints.length - 1];
        await lastNavPoint.trigger('focus');

        expect(lastNavPoint.classes()).toContain('is-tooltip-active');
    });

    it('shows an empty state with no data and honours explicit value/trend overrides', () => {
        const empty = mount(KviewPerformanceChart, {
            props: { points: [], emptyText: 'Nothing yet' },
        });

        expect(empty.find('svg').exists()).toBe(false);
        expect(empty.get('[role="status"]').text()).toBe('Nothing yet');

        const explicit = mount(KviewPerformanceChart, {
            props: {
                points,
                value: '250 M',
                valueLabel: 'Custom label',
                trendValue: '+5%',
                trendLabel: 'vs plan',
                trendDirection: 'up',
                autoTrend: false,
            },
        });

        expect(explicit.get('.fund-performance-chart__metric-value').text()).toBe('250 M');
        expect(explicit.get('.fund-performance-chart__metric-label').text()).toBe('Custom label');
        expect(explicit.get('.fund-performance-chart__trend').classes()).toContain('fund-performance-chart__trend--up');
        expect(explicit.text()).toContain('+5%');
        expect(explicit.text()).toContain('vs plan');
    });
});
