import { TestBed } from '@angular/core/testing';
import { provideRouter, ActivatedRoute, Router } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { of, throwError } from 'rxjs';
import { LibraryComponent } from './library.component';
import { ImageService } from '../services/image.service';
import { routes } from '../app.routes';

function makeActivatedRoute(queryParams: Record<string, string> = {}) {
  const paramMap = { get: (key: string) => queryParams[key] ?? null };
  return {
    snapshot: { queryParamMap: paramMap },
    queryParamMap: of(paramMap),
  };
}

const EMPTY_PAGE = { items: [], total: 0, limit: 24, offset: 0 };
const ONE_IMAGE = {
  items: [{ id: '1', short_id: 'ShrtImg1', filename: 'a.jpg', tags: ['cat'], hash: '', mime_type: 'image/jpeg', size_bytes: 1, width: 1, height: 1, storage_key: 'ShrtImg1', thumbnail_key: null, file_url: '/api/v1/i/ShrtImg1/file', thumbnail_url: null, created_at: '' }],
  total: 1, limit: 24, offset: 0,
};
const MULTI_PAGE = {
  items: Array(24).fill(null).map((_, i) => ({
    id: String(i + 1), short_id: `Shrt${String(i + 1).padStart(4, '0')}`, filename: `img${i + 1}.jpg`, tags: [], hash: '',
    mime_type: 'image/jpeg', size_bytes: 1, width: 1, height: 1,
    storage_key: `Shrt${String(i + 1).padStart(4, '0')}`, thumbnail_key: null,
    file_url: `/api/v1/i/Shrt${String(i + 1).padStart(4, '0')}/file`, thumbnail_url: null, created_at: '',
  })),
  total: 48, limit: 24, offset: 0,
};

describe('LibraryComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [LibraryComponent],
      providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter(routes)],
    }).compileComponents();
  });

  it('should render image grid from service response', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(ONE_IMAGE));
    fixture.detectChanges();
    expect((fixture.nativeElement as HTMLElement).querySelectorAll('.image-card').length).toBe(1);
  });

  it('should trigger new API call with tags param on filter change', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(EMPTY_PAGE));
    fixture.detectChanges();
    fixture.componentInstance.applyFilter(['cat', 'funny']);
    expect(listSpy).toHaveBeenCalledWith(['cat', 'funny'], jasmine.any(Number), jasmine.any(Number));
  });

  it('showSpinner is false initially', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(EMPTY_PAGE));
    fixture.detectChanges();
    expect(fixture.componentInstance.showSpinner).toBeFalse();
  });

  it('renders 8 skeleton cards while showSpinner is true', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    fixture.componentInstance.showSpinner = true;
    fixture.detectChanges();
    const skeletons = (fixture.nativeElement as HTMLElement).querySelectorAll('.card-skeleton');
    expect(skeletons.length).toBe(8);
  });

  it('error is false initially', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(EMPTY_PAGE));
    fixture.detectChanges();
    expect(fixture.componentInstance.error).toBeFalse();
  });

  it('shows error card when error is true', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(throwError(() => new Error('fail')));
    fixture.detectChanges();
    expect((fixture.nativeElement as HTMLElement).querySelector('.error-card')).not.toBeNull();
  });

  it('error card has retry button that calls load()', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(throwError(() => new Error('fail')));
    fixture.detectChanges();
    spyOn(fixture.componentInstance, 'load');
    const retryBtn = (fixture.nativeElement as HTMLElement).querySelector('.error-card .retry-btn') as HTMLButtonElement;
    expect(retryBtn).not.toBeNull();
    retryBtn.click();
    expect(fixture.componentInstance.load).toHaveBeenCalled();
  });

  it('empty state contains routerLink to /upload', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(EMPTY_PAGE));
    fixture.detectChanges();
    const link = (fixture.nativeElement as HTMLElement).querySelector('.empty-state a[href="/upload"]');
    expect(link).not.toBeNull();
  });

  it('onImgError sets src to placeholder SVG', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgEl = document.createElement('img');
    imgEl.src = 'http://example.com/image.jpg';
    const event = { target: imgEl } as unknown as Event;
    fixture.componentInstance.onImgError(event);
    expect(imgEl.src).toContain('data:image/svg+xml');
  });

  it('onImgError does not recurse when src already contains placeholder', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgEl = document.createElement('img');
    imgEl.src = 'data:image/svg+xml,placeholder';
    const originalSrc = imgEl.src;
    const event = { target: imgEl } as unknown as Event;
    fixture.componentInstance.onImgError(event);
    expect(imgEl.src).toBe(originalSrc);
  });

  it('pre-populates activeFilters from ?tags= query param on init', () => {
    TestBed.overrideProvider(ActivatedRoute, { useValue: makeActivatedRoute({ tags: 'cat,funny' }) });
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(EMPTY_PAGE));
    fixture.detectChanges();
    expect(fixture.componentInstance.activeFilters).toEqual(['cat', 'funny']);
    expect(listSpy).toHaveBeenCalledWith(['cat', 'funny'], jasmine.any(Number), jasmine.any(Number));
  });

  it('does not set activeFilters when no ?tags= param present', () => {
    TestBed.overrideProvider(ActivatedRoute, { useValue: makeActivatedRoute() });
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(EMPTY_PAGE));
    fixture.detectChanges();
    expect(fixture.componentInstance.activeFilters).toEqual([]);
  });

  it('header contains a link to /tags', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(EMPTY_PAGE));
    fixture.detectChanges();
    const link = (fixture.nativeElement as HTMLElement).querySelector('a[href="/tags"]');
    expect(link).not.toBeNull();
  });

  // ---- Pagination: page window (T002) ----

  it('pageWindow returns [1,2,3,4] on page 1 of 20', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    fixture.componentInstance.currentPage = 1;
    fixture.componentInstance.totalPages = 20;
    expect(fixture.componentInstance.pageWindow).toEqual([1, 2, 3, 4]);
  });

  it('pageWindow returns [17,18,19,20] on page 20 of 20', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    fixture.componentInstance.currentPage = 20;
    fixture.componentInstance.totalPages = 20;
    expect(fixture.componentInstance.pageWindow).toEqual([17, 18, 19, 20]);
  });

  it('pageWindow returns [6,7,8,9] on page 7 of 20', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    fixture.componentInstance.currentPage = 7;
    fixture.componentInstance.totalPages = 20;
    expect(fixture.componentInstance.pageWindow).toEqual([6, 7, 8, 9]);
  });

  it('pageWindow returns all pages when totalPages < 4', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    fixture.componentInstance.currentPage = 2;
    fixture.componentInstance.totalPages = 3;
    expect(fixture.componentInstance.pageWindow).toEqual([1, 2, 3]);
  });

  it('pageWindow returns [1] when totalPages is 1', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    fixture.componentInstance.currentPage = 1;
    fixture.componentInstance.totalPages = 1;
    expect(fixture.componentInstance.pageWindow).toEqual([1]);
  });

  it('numbered page buttons are rendered (T002)', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const pageBtns = (fixture.nativeElement as HTMLElement).querySelectorAll('.page-btn');
    expect(pageBtns.length).toBe(2); // 2 total pages
    expect(pageBtns[0].textContent?.trim()).toBe('1');
    expect(pageBtns[1].textContent?.trim()).toBe('2');
  });

  it('active page button has .active class', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const activeBtn = (fixture.nativeElement as HTMLElement).querySelector('.page-btn.active');
    expect(activeBtn).not.toBeNull();
    expect(activeBtn?.textContent?.trim()).toBe('1');
  });

  it('goToPage() calls imageService.list with correct offset', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    listSpy.calls.reset();
    fixture.componentInstance.goToPage(2);
    expect(listSpy).toHaveBeenCalledWith(jasmine.any(Array), jasmine.any(Number), 24);
  });

  it('total count renders with correct number', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const el = (fixture.nativeElement as HTMLElement).querySelector('.total-count');
    expect(el?.textContent).toContain('48');
  });

  it('no pagination controls when all images fit on one page', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(ONE_IMAGE));
    fixture.detectChanges();
    expect((fixture.nativeElement as HTMLElement).querySelector('.pagination-bar')).toBeNull();
  });

  it('nextPage() calls imageService.list with offset=24', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    listSpy.calls.reset();
    fixture.componentInstance.nextPage();
    expect(listSpy).toHaveBeenCalledWith(jasmine.any(Array), jasmine.any(Number), 24);
  });

  it('prevPage() from page 2 calls imageService.list with offset=0', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    fixture.componentInstance.currentPage = 2;
    fixture.componentInstance.totalPages = 2;
    listSpy.calls.reset();
    fixture.componentInstance.prevPage();
    expect(listSpy).toHaveBeenCalledWith(jasmine.any(Array), jasmine.any(Number), 0);
  });

  it('applyFilter() resets to page 1 (offset=0)', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    fixture.componentInstance.currentPage = 2;
    listSpy.calls.reset();
    fixture.componentInstance.applyFilter(['cat']);
    expect(listSpy).toHaveBeenCalledWith(['cat'], jasmine.any(Number), 0);
  });

  // ---- Pagination: ‹ › disabled states (T006) ----

  it('prev-btn (‹) is disabled on page 1', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const prevBtn = (fixture.nativeElement as HTMLElement).querySelector('.prev-btn') as HTMLButtonElement;
    expect(prevBtn).not.toBeNull();
    expect(prevBtn.disabled).toBeTrue();
  });

  it('next-btn (›) is disabled on last page', () => {
    TestBed.overrideProvider(ActivatedRoute, { useValue: makeActivatedRoute({ page: '2' }) });
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const nextBtn = (fixture.nativeElement as HTMLElement).querySelector('.next-btn') as HTMLButtonElement;
    expect(nextBtn).not.toBeNull();
    expect(nextBtn.disabled).toBeTrue();
  });

  it('prev-btn (‹) is enabled when not on page 1', () => {
    TestBed.overrideProvider(ActivatedRoute, { useValue: makeActivatedRoute({ page: '2' }) });
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const prevBtn = (fixture.nativeElement as HTMLElement).querySelector('.prev-btn') as HTMLButtonElement;
    expect(prevBtn.disabled).toBeFalse();
  });

  it('next-btn (›) is enabled when not on last page', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const nextBtn = (fixture.nativeElement as HTMLElement).querySelector('.next-btn') as HTMLButtonElement;
    expect(nextBtn.disabled).toBeFalse();
  });

  it('both prev and next buttons always rendered when totalPages > 1', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    expect((fixture.nativeElement as HTMLElement).querySelector('.prev-btn')).not.toBeNull();
    expect((fixture.nativeElement as HTMLElement).querySelector('.next-btn')).not.toBeNull();
  });

  // ---- Pagination: URL state ----

  it('reads ?page=2 from queryParamMap on init and calls list with offset=24', () => {
    TestBed.overrideProvider(ActivatedRoute, { useValue: makeActivatedRoute({ page: '2' }) });
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    expect(fixture.componentInstance.currentPage).toBe(2);
    expect(listSpy).toHaveBeenCalledWith(jasmine.any(Array), jasmine.any(Number), 24);
  });

  it('clamps out-of-range ?page=9999 to page 1 after load resolves', () => {
    TestBed.overrideProvider(ActivatedRoute, { useValue: makeActivatedRoute({ page: '9999' }) });
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    expect(fixture.componentInstance.currentPage).toBeLessThanOrEqual(fixture.componentInstance.totalPages);
  });

  it('nextPage() calls router.navigate with page=2 and queryParamsHandling merge', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const router = TestBed.inject(Router);
    spyOn(router, 'navigate');
    fixture.componentInstance.nextPage();
    expect(router.navigate).toHaveBeenCalledWith([], jasmine.objectContaining({
      queryParams: jasmine.objectContaining({ page: 2 }),
      queryParamsHandling: 'merge',
    }));
  });

  it('applyFilter() calls router.navigate with page=1 and queryParamsHandling merge', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    fixture.componentInstance.currentPage = 2;
    const router = TestBed.inject(Router);
    spyOn(router, 'navigate');
    fixture.componentInstance.applyFilter(['dog']);
    expect(router.navigate).toHaveBeenCalledWith([], jasmine.objectContaining({
      queryParams: jasmine.objectContaining({ page: 1 }),
      queryParamsHandling: 'merge',
    }));
  });

  it('clicking an image card navigates to /i/:short_id', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(ONE_IMAGE));
    fixture.detectChanges();
    const router = TestBed.inject(Router);
    spyOn(router, 'navigate');
    const card = (fixture.nativeElement as HTMLElement).querySelector('.image-card') as HTMLElement;
    card.click();
    expect(router.navigate).toHaveBeenCalledWith(['/i', 'ShrtImg1']);
  });

  // ---- Pagination: « » first/last (T008) ----

  it('firstPage() navigates to page 1', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    fixture.componentInstance.currentPage = 2;
    fixture.componentInstance.totalPages = 2;
    listSpy.calls.reset();
    fixture.componentInstance.firstPage();
    expect(listSpy).toHaveBeenCalledWith(jasmine.any(Array), jasmine.any(Number), 0);
    expect(fixture.componentInstance.currentPage).toBe(1);
  });

  it('lastPage() navigates to last page', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    const listSpy = spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    listSpy.calls.reset();
    fixture.componentInstance.lastPage();
    expect(fixture.componentInstance.currentPage).toBe(fixture.componentInstance.totalPages);
  });

  it('first-page button («) is disabled on page 1', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const firstBtn = (fixture.nativeElement as HTMLElement).querySelector('.first-btn') as HTMLButtonElement;
    expect(firstBtn).not.toBeNull();
    expect(firstBtn.disabled).toBeTrue();
  });

  it('last-page button (») is disabled on last page', () => {
    TestBed.overrideProvider(ActivatedRoute, { useValue: makeActivatedRoute({ page: '2' }) });
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const lastBtn = (fixture.nativeElement as HTMLElement).querySelector('.last-btn') as HTMLButtonElement;
    expect(lastBtn).not.toBeNull();
    expect(lastBtn.disabled).toBeTrue();
  });

  it('first-page button («) is enabled when not on page 1', () => {
    TestBed.overrideProvider(ActivatedRoute, { useValue: makeActivatedRoute({ page: '2' }) });
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const firstBtn = (fixture.nativeElement as HTMLElement).querySelector('.first-btn') as HTMLButtonElement;
    expect(firstBtn.disabled).toBeFalse();
  });

  it('last-page button (») is enabled when not on last page', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const imgSvc = TestBed.inject(ImageService);
    spyOn(imgSvc, 'list').and.returnValue(of(MULTI_PAGE));
    fixture.detectChanges();
    const lastBtn = (fixture.nativeElement as HTMLElement).querySelector('.last-btn') as HTMLButtonElement;
    expect(lastBtn.disabled).toBeFalse();
  });
});
