import { Component, input, output } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { By } from '@angular/platform-browser';
import { StorageMap } from '@ngx-pwa/local-storage';
import { TranslateModule } from '@ngx-translate/core';
import { of, Subject } from 'rxjs';
import { VideoPlayer } from '@iptvnator/shared/interfaces';
import { RuntimeCapabilitiesService } from '@iptvnator/services';
import type { WebPlayerViewComponent as WebPlayerViewComponentInstance } from './web-player-view.component';
import {
    PlaybackDiagnostic,
    PlaybackDiagnosticCode,
    PlaybackDiagnosticSource,
} from '../playback-diagnostics/playback-diagnostics.util';

jest.unstable_mockModule('video.js', () => ({
    default: jest.fn(),
}));

jest.unstable_mockModule('@yangkghjh/videojs-aspect-ratio-panel', () => ({}));
jest.unstable_mockModule('videojs-contrib-quality-levels', () => ({}));
jest.unstable_mockModule('videojs-quality-selector-hls', () => ({}));

@Component({
    selector: 'app-vjs-player',
    template: '<div data-test-id="stub-vjs-player"></div>',
})
class StubVjsPlayerComponent {
    readonly options = input<unknown>();
    readonly volume = input(1);
    readonly startTime = input(0);
    readonly timeUpdate = output<{ currentTime: number; duration: number }>();
    readonly playbackIssue = output<PlaybackDiagnostic | null>();
}

@Component({
    selector: 'app-html-video-player',
    template: '<div data-test-id="stub-html-player"></div>',
})
class StubHtmlVideoPlayerComponent {
    readonly channel = input<unknown>();
    readonly volume = input(1);
    readonly showCaptions = input(false);
    readonly startTime = input(0);
    readonly timeUpdate = output<{ currentTime: number; duration: number }>();
    readonly playbackIssue = output<PlaybackDiagnostic | null>();
}

@Component({
    selector: 'app-art-player',
    template: '<div data-test-id="stub-art-player"></div>',
})
class StubArtPlayerComponent {
    readonly channel = input<unknown>();
    readonly volume = input(1);
    readonly showCaptions = input(false);
    readonly startTime = input(0);
    readonly timeUpdate = output<{ currentTime: number; duration: number }>();
    readonly playbackIssue = output<PlaybackDiagnostic | null>();
}

@Component({
    selector: 'app-embedded-mpv-player',
    template: '<div data-test-id="stub-embedded-mpv-player"></div>',
})
class StubEmbeddedMpvPlayerComponent {
    readonly playback = input.required<unknown>();
    readonly recordingFolder = input('');
    readonly timeUpdate = output<{ currentTime: number; duration: number }>();
}

describe('WebPlayerViewComponent', () => {
    let WebPlayerViewComponent: typeof import('./web-player-view.component').WebPlayerViewComponent;
    let fixture: ComponentFixture<WebPlayerViewComponentInstance>;
    let component: WebPlayerViewComponentInstance;
    const storageMap = {
        get: jest.fn(() => of({ player: VideoPlayer.VideoJs })),
    };
    let runtimeCapabilities: { supportsManagedExternalPlayers: boolean };

    beforeAll(async () => {
        ({ WebPlayerViewComponent } =
            await import('./web-player-view.component'));
    });

    beforeEach(async () => {
        runtimeCapabilities = { supportsManagedExternalPlayers: false };

        await TestBed.configureTestingModule({
            imports: [WebPlayerViewComponent, TranslateModule.forRoot()],
            providers: [
                { provide: StorageMap, useValue: storageMap },
                {
                    provide: RuntimeCapabilitiesService,
                    useValue: runtimeCapabilities,
                },
            ],
        })
            .overrideComponent(WebPlayerViewComponent, {
                set: {
                    imports: [
                        StubArtPlayerComponent,
                        StubEmbeddedMpvPlayerComponent,
                        StubHtmlVideoPlayerComponent,
                        StubVjsPlayerComponent,
                        ClipboardModule,
                        MatButtonModule,
                        MatIconModule,
                        MatTooltipModule,
                        TranslateModule,
                    ],
                },
            })
            .compileComponents();

        storageMap.get.mockReturnValue(of({ player: VideoPlayer.VideoJs }));
        fixture = TestBed.createComponent(WebPlayerViewComponent);
        component = fixture.componentInstance;
        fixture.componentRef.setInput(
            'streamUrl',
            'https://example.com/archive/movie.mkv'
        );
        fixture.componentRef.setInput('title', 'Example Movie');
    });

    afterEach(() => {
        fixture.destroy();
    });

    it('anchors overlay UI to the player view host', () => {
        fixture.detectChanges();

        expect(fixture.nativeElement.classList).toContain('web-player-view');
    });

    it('renders diagnostics and emits MPV fallback requests when managed external players are available', () => {
        const requests: unknown[] = [];
        runtimeCapabilities.supportsManagedExternalPlayers = true;
        component.externalFallbackRequested.subscribe((request) =>
            requests.push(request)
        );

        fixture.detectChanges();
        component.handlePlaybackIssue(createUnsupportedContainerDiagnostic());
        fixture.detectChanges();

        const banner = fixture.debugElement.query(
            By.css('[data-test-id="playback-diagnostic-banner"]')
        );
        const mpvButton = fixture.debugElement.query(
            By.css('[data-test-id="playback-fallback-mpv"]')
        );

        expect(banner.nativeElement.textContent).toContain(
            'PLAYBACK_DIAGNOSTICS.UNSUPPORTED_CONTAINER.TITLE'
        );
        expect(banner.nativeElement.textContent).toContain(
            'PLAYBACK_DIAGNOSTICS.NATIVE_FALLBACK_TITLE'
        );
        mpvButton.nativeElement.click();

        expect(requests).toEqual([
            expect.objectContaining({
                player: 'mpv',
                playback: expect.objectContaining({
                    streamUrl: 'https://example.com/archive/movie.mkv',
                    title: 'Example Movie',
                }),
                diagnostic: expect.objectContaining({
                    code: PlaybackDiagnosticCode.UnsupportedContainer,
                }),
            }),
        ]);
    });

    it('keeps query-declared HLS streams on the HLS mime type', () => {
        const streamUrl =
            'https://example.com/play?extension=m3u8&token=signed';

        component.setVjsOptions(streamUrl);

        expect(component.vjsOptions.sources).toEqual([
            {
                src: streamUrl,
                type: 'application/x-mpegURL',
            },
        ]);
    });

    it('treats web script playback URLs without declared media extension as MPEG-TS', () => {
        const streamUrl = 'https://example.com/live.php?stream=123&token=x';

        component.setVjsOptions(streamUrl);

        expect(component.vjsOptions.sources).toEqual([
            {
                src: streamUrl,
                type: 'video/mp2t',
            },
        ]);
    });

    it('marks portal VOD playback as non-live for Video.js MPEG-TS playback', () => {
        const streamUrl = 'https://example.com/movie/123.ts';
        fixture.componentRef.setInput('playback', {
            streamUrl,
            title: 'Example Movie',
            contentInfo: {
                playlistId: 'playlist-1',
                contentXtreamId: 123,
                contentType: 'vod',
            },
        });

        fixture.detectChanges();

        const player = fixture.debugElement.query(
            By.directive(StubVjsPlayerComponent)
        ).componentInstance as StubVjsPlayerComponent;
        expect(player.options()).toEqual(
            expect.objectContaining({
                isLive: false,
                sources: [
                    {
                        src: streamUrl,
                        type: 'video/mp2t',
                    },
                ],
            })
        );
    });

    it('preserves playback HTTP metadata for channel-based players', () => {
        const streamUrl = 'https://example.com/live/channel.m3u8';
        fixture.componentRef.setInput(
            'playerOverride',
            VideoPlayer.Html5Player
        );
        fixture.componentRef.setInput('playback', {
            streamUrl,
            title: 'Header Locked Channel',
            userAgent: 'ProviderAgent/1.0',
            referer: 'https://provider.example/ref',
            origin: 'https://provider.example',
            headers: {
                'User-Agent': 'IgnoredFallbackAgent/1.0',
                Referer: 'https://ignored.example/ref',
                Origin: 'https://ignored.example',
            },
        });

        fixture.detectChanges();

        const player = fixture.debugElement.query(
            By.directive(StubHtmlVideoPlayerComponent)
        ).componentInstance as StubHtmlVideoPlayerComponent;
        expect(player.channel()).toEqual(
            expect.objectContaining({
                url: streamUrl,
                name: 'Header Locked Channel',
                http: {
                    'user-agent': 'ProviderAgent/1.0',
                    referrer: 'https://provider.example/ref',
                    origin: 'https://provider.example',
                },
            })
        );
    });

    it('falls back to playback headers when explicit HTTP metadata is absent', () => {
        const streamUrl = 'https://example.com/live/channel.m3u8';
        fixture.componentRef.setInput(
            'playerOverride',
            VideoPlayer.Html5Player
        );
        fixture.componentRef.setInput('playback', {
            streamUrl,
            title: 'Header Fallback Channel',
            headers: {
                'user-agent': 'HeaderAgent/1.0',
                referer: 'https://headers.example/ref',
                origin: 'https://headers.example',
            },
        });

        fixture.detectChanges();

        const player = fixture.debugElement.query(
            By.directive(StubHtmlVideoPlayerComponent)
        ).componentInstance as StubHtmlVideoPlayerComponent;
        expect(player.channel()).toEqual(
            expect.objectContaining({
                http: {
                    'user-agent': 'HeaderAgent/1.0',
                    referrer: 'https://headers.example/ref',
                    origin: 'https://headers.example',
                },
            })
        );
    });

    it('renders embedded MPV before settings storage emits', () => {
        fixture.destroy();

        const pendingSettings = new Subject<unknown>();
        storageMap.get.mockReturnValue(pendingSettings.asObservable());
        fixture = TestBed.createComponent(WebPlayerViewComponent);
        component = fixture.componentInstance;
        fixture.componentRef.setInput(
            'streamUrl',
            'https://example.com/archive/movie.mkv'
        );
        fixture.componentRef.setInput('title', 'Example Movie');
        fixture.componentRef.setInput(
            'playerOverride',
            VideoPlayer.EmbeddedMpv
        );

        expect(() => fixture.detectChanges()).not.toThrow();

        const player = fixture.debugElement.query(
            By.directive(StubEmbeddedMpvPlayerComponent)
        ).componentInstance as StubEmbeddedMpvPlayerComponent;
        expect(player.recordingFolder()).toBe('');
    });

    it('uses the PWA browser access diagnostic description key outside desktop', () => {
        const issue = createBrowserAccessDiagnostic();

        expect(component.getDiagnosticTitleKey(issue)).toBe(
            'PLAYBACK_DIAGNOSTICS.BROWSER_ACCESS_ERROR.TITLE'
        );
        expect(component.getDiagnosticDescriptionKey(issue)).toBe(
            'PLAYBACK_DIAGNOSTICS.BROWSER_ACCESS_ERROR.PWA_DESCRIPTION'
        );
    });

    it('keeps the desktop browser access diagnostic description key', () => {
        runtimeCapabilities.supportsManagedExternalPlayers = true;
        const issue = createBrowserAccessDiagnostic();

        expect(component.getDiagnosticDescriptionKey(issue)).toBe(
            'PLAYBACK_DIAGNOSTICS.BROWSER_ACCESS_ERROR.DESCRIPTION'
        );
    });

    it('uses an inline recovery headline when external fallback actions are unavailable', () => {
        fixture.detectChanges();
        component.handlePlaybackIssue(createNetworkDiagnostic());
        fixture.detectChanges();

        const banner = fixture.debugElement.query(
            By.css('[data-test-id="playback-diagnostic-banner"]')
        );
        const mpvButton = fixture.debugElement.query(
            By.css('[data-test-id="playback-fallback-mpv"]')
        );

        expect(mpvButton).toBeNull();
        expect(banner.nativeElement.textContent).toContain(
            'PLAYBACK_DIAGNOSTICS.NETWORK_ERROR.TITLE'
        );
        expect(banner.nativeElement.textContent).toContain(
            'PLAYBACK_DIAGNOSTICS.INLINE_FAILURE_TITLE'
        );
    });

    it('renders technical details and codec-specific hints in the diagnostic banner', () => {
        fixture.detectChanges();
        const issue = createUnsupportedCodecDiagnostic();

        component.handlePlaybackIssue(issue);
        fixture.detectChanges();

        const details = fixture.debugElement.query(
            By.css('[data-test-id="playback-diagnostic-details"]')
        );
        const codecHint = fixture.debugElement.query(
            By.css('[data-test-id="playback-codec-hint"]')
        );

        expect(details.nativeElement.textContent).toContain(
            'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY'
        );
        expect(component.getDiagnosticCodecHint(issue)).toBe('HEVC, AC-3');
        expect(component.getDiagnosticDetails(issue)).toEqual(
            expect.arrayContaining([
                {
                    labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_CODE',
                    value: 'unsupported-codec',
                },
                {
                    labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_PLAYER',
                    value: 'Video.js',
                },
                {
                    labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_VIDEO_CODECS',
                    value: 'hvc1.1.6.L93.B0',
                },
                {
                    labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_AUDIO_CODECS',
                    value: 'ac-3',
                },
            ])
        );
        expect(codecHint.nativeElement.textContent).toContain(
            'PLAYBACK_DIAGNOSTICS.CODEC_HINT'
        );
    });

    it('clears playback diagnostics when retrying inline playback', () => {
        fixture.detectChanges();
        const player = fixture.debugElement.query(
            By.directive(StubVjsPlayerComponent)
        ).componentInstance as StubVjsPlayerComponent;
        expect(player.options()).toEqual(
            expect.objectContaining({ reloadToken: 0 })
        );

        component.handlePlaybackIssue(createUnsupportedCodecDiagnostic());
        fixture.detectChanges();

        const retryButton = fixture.debugElement.query(
            By.css('[data-test-id="playback-retry"]')
        );
        const utilityControls = fixture.nativeElement.querySelectorAll(
            '[data-test-id="playback-retry"], [data-test-id="playback-diagnostic-details"]'
        );

        expect(utilityControls[0]).toBe(retryButton.nativeElement);

        retryButton.nativeElement.click();
        fixture.detectChanges();

        expect(component.playbackDiagnostic()).toBeNull();
        expect(player.options()).toEqual(
            expect.objectContaining({ reloadToken: 1 })
        );
        expect(
            fixture.debugElement.query(
                By.css('[data-test-id="playback-diagnostic-banner"]')
            )
        ).toBeNull();
    });
});

function createUnsupportedContainerDiagnostic(): PlaybackDiagnostic {
    return {
        code: PlaybackDiagnosticCode.UnsupportedContainer,
        source: PlaybackDiagnosticSource.Native,
        sourceUrl: 'https://example.com/archive/movie.mkv',
        container: 'mkv',
        mimeType: 'video/matroska',
        player: 'videojs',
        audioCodecs: [],
        videoCodecs: [],
        externalFallbackRecommended: true,
    };
}

function createBrowserAccessDiagnostic(): PlaybackDiagnostic {
    return {
        code: PlaybackDiagnosticCode.BrowserAccessError,
        source: PlaybackDiagnosticSource.Hls,
        sourceUrl: 'https://provider.example/live.m3u8',
        container: 'm3u8',
        mimeType: 'application/x-mpegURL',
        player: 'videojs',
        audioCodecs: [],
        videoCodecs: [],
        details: 'blocked by CORS policy',
        externalFallbackRecommended: true,
    };
}

function createUnsupportedCodecDiagnostic(): PlaybackDiagnostic {
    return {
        code: PlaybackDiagnosticCode.UnsupportedCodec,
        source: PlaybackDiagnosticSource.Hls,
        sourceUrl: 'https://example.com/live/index.m3u8',
        container: 'm3u8',
        mimeType: 'application/x-mpegURL',
        player: 'videojs',
        audioCodecs: ['ac-3'],
        videoCodecs: ['hvc1.1.6.L93.B0'],
        details: 'manifestIncompatibleCodecsError',
        externalFallbackRecommended: true,
    };
}

function createNetworkDiagnostic(): PlaybackDiagnostic {
    return {
        code: PlaybackDiagnosticCode.NetworkError,
        source: PlaybackDiagnosticSource.MpegTs,
        sourceUrl: 'https://example.com/live/channel.ts',
        container: 'ts',
        mimeType: 'video/mp2t',
        player: 'videojs',
        audioCodecs: [],
        videoCodecs: [],
        details: 'HttpStatusCodeInvalid {"code":456,"msg":"<none>"}',
        externalFallbackRecommended: false,
    };
}
