import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import {
  HttpTestingController,
  provideHttpClientTesting,
} from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import { SavedView } from 'src/app/data/saved-view'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import {
  DjangoMessageLevel,
  DjangoMessagesService,
} from 'src/app/services/django-messages.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import {
  PermissionType,
  PermissionsService,
} from 'src/app/services/permissions.service'
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service'
import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
import { AppFrameComponent } from './app-frame.component'
import { GlobalSearchComponent } from './global-search/global-search.component'

const saved_views = [
  {
    name: 'Saved View 0',
    id: 0,
    show_on_dashboard: true,
    show_in_sidebar: true,
    sort_field: 'name',
    sort_reverse: true,
    filter_rules: [],
  },
  {
    name: 'Saved View 1',
    id: 1,
    show_on_dashboard: false,
    show_in_sidebar: false,
    sort_field: 'name',
    sort_reverse: true,
    filter_rules: [],
  },
  {
    name: 'Saved View 2',
    id: 2,
    show_on_dashboard: true,
    show_in_sidebar: true,
    sort_field: 'name',
    sort_reverse: true,
    filter_rules: [],
  },
  {
    name: 'Saved View 3',
    id: 3,
    show_on_dashboard: true,
    show_in_sidebar: true,
    sort_field: 'name',
    sort_reverse: true,
    filter_rules: [],
  },
]
const document = { id: 2, title: 'Hello world' }

describe('AppFrameComponent', () => {
  let component: AppFrameComponent
  let fixture: ComponentFixture<AppFrameComponent>
  let httpTestingController: HttpTestingController
  let settingsService: SettingsService
  let permissionsService: PermissionsService
  let remoteVersionService: RemoteVersionService
  let toastService: ToastService
  let messagesService: DjangoMessagesService
  let openDocumentsService: OpenDocumentsService
  let router: Router
  let savedViewSpy
  let modalService: NgbModal
  let maybeRefreshSpy
  let tasksService: TasksService

  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [
        BrowserModule,
        RouterTestingModule.withRoutes(routes),
        NgbModule,
        FormsModule,
        ReactiveFormsModule,
        DragDropModule,
        NgbModalModule,
        NgxBootstrapIconsModule.pick(allIcons),
        AppFrameComponent,
        IfPermissionsDirective,
        GlobalSearchComponent,
      ],
      providers: [
        SettingsService,
        {
          provide: SavedViewService,
          useValue: {
            reload: (fn: any) => {
              if (fn) {
                fn()
              }
            },
            listAll: () =>
              of({
                all: [saved_views.map((v) => v.id)],
                count: saved_views.length,
                results: saved_views,
              }),
            sidebarViews: saved_views.filter((v) => v.show_in_sidebar),
            getDocumentCount: (view: SavedView) => 5,
            maybeRefreshDocumentCounts: () => {},
          },
        },
        PermissionsService,
        RemoteVersionService,
        IfPermissionsDirective,
        ToastService,
        DjangoMessagesService,
        OpenDocumentsService,
        SearchService,
        NgbModal,
        {
          provide: ActivatedRoute,
          useValue: {
            firstChild: {
              component: DocumentDetailComponent,
            },
            snapshot: {
              firstChild: {
                component: DocumentDetailComponent,
                params: {
                  id: document.id,
                },
              },
            },
          },
        },
        PermissionsGuard,
        provideHttpClient(withInterceptorsFromDi()),
        provideHttpClientTesting(),
        provideUiTour(),
      ],
    }).compileComponents()

    settingsService = TestBed.inject(SettingsService)
    const savedViewService = TestBed.inject(SavedViewService)
    permissionsService = TestBed.inject(PermissionsService)
    remoteVersionService = TestBed.inject(RemoteVersionService)
    toastService = TestBed.inject(ToastService)
    messagesService = TestBed.inject(DjangoMessagesService)
    openDocumentsService = TestBed.inject(OpenDocumentsService)
    modalService = TestBed.inject(NgbModal)
    router = TestBed.inject(Router)
    tasksService = TestBed.inject(TasksService)

    jest
      .spyOn(settingsService, 'displayName', 'get')
      .mockReturnValue('Hello World')
    jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)

    savedViewSpy = jest.spyOn(savedViewService, 'reload')
    maybeRefreshSpy = jest.spyOn(savedViewService, 'maybeRefreshDocumentCounts')

    fixture = TestBed.createComponent(AppFrameComponent)
    component = fixture.componentInstance

    httpTestingController = TestBed.inject(HttpTestingController)

    fixture.detectChanges()
  })

  it('should initialize the saved view service', () => {
    expect(savedViewSpy).toHaveBeenCalled()
  })

  it('should update reinitialized signal-backed settings without manual change detection', async () => {
    settingsService.initializeSettings().subscribe()
    httpTestingController
      .expectOne(`${environment.apiBaseUrl}ui_settings/`)
      .flush({
        settings: { app_title: 'Reactive title' },
        user: {},
        permissions: [],
      })

    await fixture.whenStable()

    expect(
      fixture.nativeElement.querySelector('.brand-title').textContent
    ).toBe('Reactive title')
  })

  it('should check for update if enabled', () => {
    const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
    updateCheckSpy.mockImplementation(() => {
      return of({
        version: 'v100.0',
        update_available: true,
      })
    })
    settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, true)
    component.ngOnInit()
    expect(updateCheckSpy).toHaveBeenCalled()
    fixture.detectChanges()
    expect(fixture.nativeElement.textContent).toContain('Update available')
  })

  it('should check not for update if disabled', () => {
    const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
    settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
    component.ngOnInit()
    fixture.detectChanges()
    expect(updateCheckSpy).not.toHaveBeenCalled()
    expect(fixture.nativeElement.textContent).not.toContain('Update available')
  })

  it('should check for update if was disabled and then enabled', () => {
    const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
    settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
    component.setUpdateChecking(true)
    fixture.detectChanges()
    expect(updateCheckSpy).toHaveBeenCalled()
  })

  it('should show error on toggle update checking if store settings fails', () => {
    jest.spyOn(console, 'warn').mockImplementation(() => {})
    const toastSpy = jest.spyOn(toastService, 'showError')
    settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
    component.setUpdateChecking(true)
    httpTestingController
      .expectOne(`${environment.apiBaseUrl}ui_settings/`)
      .flush('error', {
        status: 500,
        statusText: 'error',
      })
    expect(toastSpy).toHaveBeenCalled()
  })

  it('should support toggling slim sidebar and saving', () => {
    jest.useFakeTimers()
    const saveSettingSpy = jest.spyOn(settingsService, 'set')
    settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
    expect(component.slimSidebarEnabled).toBeFalsy()
    expect(component.slimSidebarAnimating()).toBeFalsy()
    component.toggleSlimSidebar()
    const requests = httpTestingController.match(
      `${environment.apiBaseUrl}ui_settings/`
    )
    expect(requests).toHaveLength(1)
    expect(requests[0].request.body.settings.slim_sidebar).toBe(true)
    expect(
      requests[0].request.body.settings.attributes_sections_collapsed
    ).toEqual(['attributes'])
    requests[0].flush({ success: true })
    expect(component.slimSidebarAnimating()).toBeTruthy()
    jest.advanceTimersByTime(200)
    expect(component.slimSidebarAnimating()).toBeFalsy()
    expect(component.slimSidebarEnabled).toBeTruthy()
    expect(saveSettingSpy).toHaveBeenCalledWith(
      SETTINGS_KEYS.SLIM_SIDEBAR,
      true
    )
    expect(saveSettingSpy).toHaveBeenCalledWith(
      SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
      ['attributes']
    )
    jest.useRealTimers()
  })

  it('should show error on toggle slim sidebar if store settings fails', () => {
    jest.spyOn(console, 'warn').mockImplementation(() => {})
    const toastSpy = jest.spyOn(toastService, 'showError')
    component.toggleSlimSidebar()
    httpTestingController
      .match(`${environment.apiBaseUrl}ui_settings/`)[0]
      .flush('error', {
        status: 500,
        statusText: 'error',
      })
    expect(toastSpy).toHaveBeenCalled()
  })

  it('should support collapsible menu', () => {
    const button: HTMLButtonElement = (
      fixture.nativeElement as HTMLDivElement
    ).querySelector('button[data-toggle=collapse]')
    button.dispatchEvent(new MouseEvent('click'))
    expect(component.isMenuCollapsed()).toBeFalsy()
    component.closeMenu()
    expect(component.isMenuCollapsed()).toBeTruthy()
  })

  it('should hide mobile search when scrolling down and show it when scrolling up', () => {
    Object.defineProperty(globalThis, 'innerWidth', {
      value: 767,
    })

    component.ngOnInit()

    Object.defineProperty(globalThis, 'scrollY', {
      configurable: true,
      value: 40,
    })
    component.onWindowScroll()
    expect(component.mobileSearchHidden()).toBe(true)

    Object.defineProperty(globalThis, 'scrollY', {
      configurable: true,
      value: 0,
    })
    component.onWindowScroll()
    expect(component.mobileSearchHidden()).toBe(false)
  })

  it('should keep mobile search visible on desktop scroll or resize', () => {
    Object.defineProperty(globalThis, 'innerWidth', {
      value: 1024,
    })
    component.ngOnInit()
    component.mobileSearchHidden.set(true)

    component.onWindowScroll()

    expect(component.mobileSearchHidden()).toBe(false)

    component.mobileSearchHidden.set(true)
    component.onWindowResize()
  })

  it('should keep mobile search visible while the mobile menu is expanded', () => {
    Object.defineProperty(globalThis, 'innerWidth', {
      value: 767,
    })
    component.ngOnInit()
    component.isMenuCollapsed.set(false)

    Object.defineProperty(globalThis, 'scrollY', {
      configurable: true,
      value: 40,
    })
    component.onWindowScroll()

    expect(component.mobileSearchHidden()).toBe(false)
  })

  it('should support close document & navigate on close current doc', () => {
    const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
    closeSpy.mockReturnValue(of(true))
    const routerSpy = jest.spyOn(router, 'navigate')
    component.closeDocument(document)
    expect(closeSpy).toHaveBeenCalledWith(document)
    expect(routerSpy).toHaveBeenCalled()
  })

  it('should support close all documents & navigate on close current doc', () => {
    const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
    closeAllSpy.mockReturnValue(of(true))
    const routerSpy = jest.spyOn(router, 'navigate')
    component.closeAll()
    expect(closeAllSpy).toHaveBeenCalled()
    expect(routerSpy).toHaveBeenCalled()
  })

  it('should close all documents on logout', () => {
    const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
    component.onLogout()
    expect(closeAllSpy).toHaveBeenCalled()
  })

  it('should warn before close if dirty documents', () => {
    jest.spyOn(openDocumentsService, 'hasDirty').mockReturnValue(true)
    expect(component.canDeactivate()).toBeFalsy()
  })

  it('should disable global dropzone on start drag + drop, re-enable after', () => {
    expect(settingsService.globalDropzoneEnabled()).toBeTruthy()
    component.onDragStart(null)
    expect(settingsService.globalDropzoneEnabled()).toBeFalsy()
    component.onDragEnd(null)
    expect(settingsService.globalDropzoneEnabled()).toBeTruthy()
  })

  it('should update saved view sorting on drag + drop, show info', () => {
    const settingsSpy = jest.spyOn(settingsService, 'updateSidebarViewsSort')
    const toastSpy = jest.spyOn(toastService, 'showInfo')
    jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
    component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
      SavedView[]
    >)
    expect(settingsSpy).toHaveBeenCalledWith([
      saved_views[2],
      saved_views[0],
      saved_views[3],
    ])
    expect(toastSpy).toHaveBeenCalled()
  })

  it('should update saved view sorting on drag + drop, show error', () => {
    jest.spyOn(settingsService, 'get').mockImplementation((key) => {
      if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER) return []
    })
    fixture.destroy()
    fixture = TestBed.createComponent(AppFrameComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
    const toastSpy = jest.spyOn(toastService, 'showError')
    jest
      .spyOn(settingsService, 'storeSettings')
      .mockReturnValue(throwError(() => new Error('unable to save')))
    component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
      SavedView[]
    >)
    expect(toastSpy).toHaveBeenCalled()
  })

  it('should support edit profile', () => {
    const modalSpy = jest.spyOn(modalService, 'open')
    component.editProfile()
    expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
      backdrop: 'static',
      size: 'xl',
    })
  })

  it('should show toasts for django messages', () => {
    const toastErrorSpy = jest.spyOn(toastService, 'showError')
    const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
    jest.spyOn(messagesService, 'get').mockReturnValue([
      { level: DjangoMessageLevel.WARNING, message: 'Test warning' },
      { level: DjangoMessageLevel.ERROR, message: 'Test error' },
      { level: DjangoMessageLevel.SUCCESS, message: 'Test success' },
      { level: DjangoMessageLevel.INFO, message: 'Test info' },
      { level: DjangoMessageLevel.DEBUG, message: 'Test debug' },
    ])
    component.ngOnInit()
    expect(toastErrorSpy).toHaveBeenCalledTimes(2)
    expect(toastInfoSpy).toHaveBeenCalledTimes(3)
  })

  it('should call maybeRefreshDocumentCounts after saved views reload', () => {
    expect(maybeRefreshSpy).toHaveBeenCalled()
  })

  it('should show tasks badge for needs-attention tasks', () => {
    jest
      .spyOn(tasksService, 'needsAttentionTasks', 'get')
      .mockReturnValue([{} as any, {} as any])

    fixture.detectChanges()

    expect(fixture.nativeElement.textContent).toContain('Tasks2')
  })

  it('should indicate attributes management availability when any permission is granted', () => {
    jest
      .spyOn(permissionsService, 'currentUserCan')
      .mockImplementation((action, type) => {
        return type === PermissionType.Tag
      })

    expect(component.canManageAttributes).toBe(true)
  })

  it('should indicate attributes management availability for other permission types', () => {
    const canSpy = jest
      .spyOn(permissionsService, 'currentUserCan')
      .mockImplementation((action, type) => {
        return type === PermissionType.Correspondent
      })
    expect(component.canManageAttributes).toBe(true)

    canSpy.mockImplementation((action, type) => {
      return type === PermissionType.DocumentType
    })
    expect(component.canManageAttributes).toBe(true)

    canSpy.mockImplementation((action, type) => {
      return type === PermissionType.StoragePath
    })
    expect(component.canManageAttributes).toBe(true)

    canSpy.mockImplementation((action, type) => {
      return type === PermissionType.CustomField
    })
    expect(component.canManageAttributes).toBe(true)
  })

  it('should toggle attributes sections and stop event bubbling', () => {
    const preventDefault = jest.fn()
    const stopPropagation = jest.fn()
    const setSpy = jest.spyOn(settingsService, 'set')
    jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))

    component.toggleAttributesSections({
      preventDefault,
      stopPropagation,
    } as any)

    expect(preventDefault).toHaveBeenCalled()
    expect(stopPropagation).toHaveBeenCalled()
    expect(setSpy).toHaveBeenCalledWith(
      SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
      ['attributes']
    )
  })

  it('should show error when saving slim sidebar setting fails', () => {
    const toastSpy = jest.spyOn(toastService, 'showError')
    jest.spyOn(console, 'warn').mockImplementation(() => {})
    jest
      .spyOn(settingsService, 'storeSettings')
      .mockReturnValue(throwError(() => new Error('boom')))

    component.slimSidebarEnabled = true

    expect(toastSpy).toHaveBeenCalled()
  })

  it('should show error when saving attributes collapsed setting fails', () => {
    const toastSpy = jest.spyOn(toastService, 'showError')
    jest.spyOn(console, 'warn').mockImplementation(() => {})
    jest
      .spyOn(settingsService, 'storeSettings')
      .mockReturnValue(throwError(() => new Error('boom')))

    component.attributesSectionsCollapsed = true

    expect(toastSpy).toHaveBeenCalled()
  })

  it('should persist attributes section collapse state', () => {
    const setSpy = jest.spyOn(settingsService, 'set')
    jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))

    component.attributesSectionsCollapsed = true

    expect(setSpy).toHaveBeenCalledWith(
      SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
      ['attributes']
    )
  })

  it('should only flag scrollbars that take up layout width', () => {
    const offsetWidth = jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get')
    jest.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(100)

    offsetWidth.mockReturnValue(115)
    component['detectClassicScrollbars']()
    expect(
      window.document.documentElement.classList.contains(
        'pngx-classic-scrollbars'
      )
    ).toBeTruthy()

    offsetWidth.mockReturnValue(100)
    component['detectClassicScrollbars']()
    expect(
      window.document.documentElement.classList.contains(
        'pngx-classic-scrollbars'
      )
    ).toBeFalsy()
  })

  it('should collapse attributes sections when enabling slim sidebar', () => {
    jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
    settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
    settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, false)

    component.toggleSlimSidebar()

    expect(component.attributesSectionsCollapsed).toBe(true)
  })
})
