import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { signal } from '@angular/core';
import { provideRouter } from '@angular/router';
import { of } from 'rxjs';
import { AdminOrderWatcherService } from './admin-order-watcher.service';
import { AdminOrdersGateway } from '../../orders/services/admin-orders-gateway.interface';
import { ADMIN_ORDERS_GATEWAY } from '../../orders/services/admin-orders-gateway.token';
import { AdminOrder, AdminOrdersListResult } from '../../orders/models/admin-order.model';
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
import { AdminAuthService } from '@marketplaces/auth';

function makeOrder(id: string, orderNumber: string, createdAt: string): AdminOrder {
  return {
    id,
    orderNumber,
    status: 'pending',
    customer: { name: 'Test Customer', email: 't@example.com', phone: '+70000000000' },
    payment: { method: 'card', status: 'paid', amount: 1000, currency: 'RUB' },
    shipping: { address: '', method: '', trackingNumber: '' },
    items: [],
    total: 1000,
    currency: 'RUB',
    notes: '',
    internalNotes: '',
    timeline: [],
    archived: false,
    createdAt,
    updatedAt: createdAt,
  };
}

describe('AdminOrderWatcherService', () => {
  let ordersByPoll: AdminOrder[][];
  let pollIndex: number;
  let notifications: UserNotificationService;
  let service: AdminOrderWatcherService;
  let fakeAdminAuth: { isAuthenticated: ReturnType<typeof signal<boolean>> };

  function fakeGateway() {
    return {
      loadOrders: () => {
        const items = ordersByPoll[pollIndex] ?? ordersByPoll[ordersByPoll.length - 1];
        const result: AdminOrdersListResult = { items, total: items.length, page: 1, pageSize: 20 };
        return of(result);
      },
    };
  }

  beforeEach(() => {
    pollIndex = 0;
    ordersByPoll = [
      [makeOrder('o2', '1002', '2026-08-15T10:00:00.000Z'), makeOrder('o1', '1001', '2026-08-15T09:00:00.000Z')],
    ];

    localStorage.removeItem('adminOrderWatcher.lastNotifiedOrderId.v1');
    localStorage.removeItem('adminOrderWatcher.lastNotifiedOrderCreatedAt.v1');
    localStorage.removeItem('adminOrderWatcher.lastAcknowledgedOrderId.v1');
    localStorage.removeItem('adminOrderWatcher.lastAcknowledgedOrderCreatedAt.v1');
    localStorage.removeItem('adminOrderWatcher.pollIntervalMs.v1');

    // Defaults to unauthenticated so the reactive effect stays a no-op and the
    // pre-existing tests below (which drive polling via explicit start()/stop()
    // calls) keep their original behavior unchanged.
    fakeAdminAuth = { isAuthenticated: signal(false) };

    TestBed.configureTestingModule({
      providers: [
        provideRouter([]),
        { provide: ADMIN_ORDERS_GATEWAY, useValue: fakeGateway() as unknown as AdminOrdersGateway },
        { provide: AdminAuthService, useValue: fakeAdminAuth as unknown as AdminAuthService },
      ],
    });

    notifications = TestBed.inject(UserNotificationService);
    service = TestBed.inject(AdminOrderWatcherService);
  });

  it('does not toast on the very first poll and marks everything as acknowledged', fakeAsync(() => {
    service.start();
    tick(0);

    expect(notifications.notifications().length).toBe(0);
    expect(service.unreadCount()).toBe(0);
    expect(service.recentOrders().map((o: AdminOrder) => o.id)).toEqual(['o2', 'o1']);
  }));

  it('toasts and increments unreadCount for orders newer than the last-notified one', fakeAsync(() => {
    service.start();
    tick(0);

    pollIndex = 1;
    ordersByPoll.push([
      makeOrder('o3', '1003', '2026-08-15T11:00:00.000Z'),
      makeOrder('o2', '1002', '2026-08-15T10:00:00.000Z'),
      makeOrder('o1', '1001', '2026-08-15T09:00:00.000Z'),
    ]);

    tick(service.intervalMs());

    expect(notifications.notifications().length).toBe(1);
    expect(notifications.notifications()[0].message).toContain('1003');
    expect(notifications.notifications()[0].route).toEqual(['ru', 'backoffice', 'orders', 'o3']);
    expect(service.unreadCount()).toBe(1);
  }));

  it('markAllSeen resets unreadCount without clearing recentOrders', fakeAsync(() => {
    service.start();
    tick(0);

    pollIndex = 1;
    ordersByPoll.push([
      makeOrder('o3', '1003', '2026-08-15T11:00:00.000Z'),
      makeOrder('o2', '1002', '2026-08-15T10:00:00.000Z'),
      makeOrder('o1', '1001', '2026-08-15T09:00:00.000Z'),
    ]);
    tick(service.intervalMs());

    expect(service.unreadCount()).toBe(1);

    service.markAllSeen();

    expect(service.unreadCount()).toBe(0);
    expect(service.recentOrders().map((o: AdminOrder) => o.id)).toEqual(['o3', 'o2', 'o1']);
  }));

  it('setIntervalSeconds updates intervalMs and rejects invalid values', () => {
    expect(service.setIntervalSeconds(30)).toBe(true);
    expect(service.intervalMs()).toBe(30000);

    expect(service.setIntervalSeconds(0)).toBe(false);
    expect(service.intervalMs()).toBe(30000);

    expect(service.setIntervalSeconds(-5)).toBe(false);
    expect(service.intervalMs()).toBe(30000);
  });

  it('falls back to a timestamp comparison (not the whole page) when the last-notified order id disappears', fakeAsync(() => {
    service.start();
    tick(0);

    // Second poll: 'o2' (the last-notified id) is gone. Page contains orders
    // both older and newer than o2's createdAt (2026-08-15T10:00:00.000Z).
    pollIndex = 1;
    ordersByPoll.push([
      makeOrder('o5', '1005', '2026-08-15T12:00:00.000Z'),
      makeOrder('o4', '1004', '2026-08-15T11:00:00.000Z'),
      makeOrder('o3', '1003', '2026-08-15T10:30:00.000Z'),
      makeOrder('o0', '1000', '2026-08-15T08:00:00.000Z'),
    ]);

    tick(service.intervalMs());

    // Only o5, o4, o3 are newer than o2's createdAt - o0 must not toast.
    expect(notifications.notifications().length).toBe(3);
    const messages = notifications.notifications().map(n => n.message);
    expect(messages.some(m => m.includes('1005'))).toBe(true);
    expect(messages.some(m => m.includes('1004'))).toBe(true);
    expect(messages.some(m => m.includes('1003'))).toBe(true);
    expect(messages.some(m => m.includes('1000'))).toBe(false);
  }));

  it('falls back to a timestamp comparison (not the whole page) for unreadCount when the acknowledged order id disappears', fakeAsync(() => {
    service.start();
    tick(0);

    // Second poll introduces 'o3' and pushes 'o2' to index 1.
    pollIndex = 1;
    ordersByPoll.push([
      makeOrder('o3', '1003', '2026-08-15T11:00:00.000Z'),
      makeOrder('o2', '1002', '2026-08-15T10:00:00.000Z'),
      makeOrder('o1', '1001', '2026-08-15T09:00:00.000Z'),
    ]);
    tick(service.intervalMs());

    // Acknowledge everything up to and including 'o3' (createdAt 11:00:00).
    service.markAllSeen();
    expect(service.unreadCount()).toBe(0);

    // Third poll: 'o3' (the acknowledged id) is gone. Page contains orders
    // both older and newer than o3's createdAt (2026-08-15T11:00:00.000Z).
    pollIndex = 2;
    ordersByPoll.push([
      makeOrder('o6', '1006', '2026-08-15T13:00:00.000Z'),
      makeOrder('o5', '1005', '2026-08-15T12:00:00.000Z'),
      makeOrder('o4', '1004', '2026-08-15T10:30:00.000Z'),
      makeOrder('o0', '1000', '2026-08-15T08:00:00.000Z'),
    ]);
    tick(service.intervalMs());

    // Only o6 and o5 are newer than o3's ack createdAt - the buggy behavior
    // would report the full page length (4) instead of the correct count (2).
    expect(service.unreadCount()).toBe(2);
  }));

  it('stop() clears the interval so no further polls occur', fakeAsync(() => {
    const gateway = TestBed.inject(ADMIN_ORDERS_GATEWAY);
    const loadOrdersSpy = spyOn(gateway, 'loadOrders').and.callThrough();

    service.start();
    tick(0);
    const callsAfterStart = loadOrdersSpy.calls.count();

    tick(service.intervalMs() / 2);
    service.stop();
    tick(service.intervalMs() * 2);

    expect(loadOrdersSpy.calls.count()).toBe(callsAfterStart);
  }));

  it('reacts to AdminAuthService.isAuthenticated: starts, stops on logout, and resumes on re-login', fakeAsync(() => {
    const gateway = TestBed.inject(ADMIN_ORDERS_GATEWAY);
    const loadOrdersSpy = spyOn(gateway, 'loadOrders').and.callThrough();

    // Starts authenticated -> the effect should start polling on its own,
    // with no explicit service.start() call from a component.
    fakeAdminAuth.isAuthenticated.set(true);
    TestBed.flushEffects();
    tick(0);

    expect(loadOrdersSpy.calls.count()).toBeGreaterThan(0);
    const callsWhileAuthenticated = loadOrdersSpy.calls.count();

    // Logout: flip the signal to false. Polling must actually stop - this is
    // the gap the previous DestroyRef-based fix failed to close, since nothing
    // destroys AdminLayoutComponent on logout.
    fakeAdminAuth.isAuthenticated.set(false);
    TestBed.flushEffects();
    tick(0);
    const callsRightAfterLogout = loadOrdersSpy.calls.count();

    tick(service.intervalMs() * 3);
    expect(loadOrdersSpy.calls.count()).toBe(callsRightAfterLogout);

    // Re-login: polling should resume, not stay permanently stopped.
    fakeAdminAuth.isAuthenticated.set(true);
    TestBed.flushEffects();
    tick(0);
    tick(service.intervalMs());

    expect(loadOrdersSpy.calls.count()).toBeGreaterThan(callsWhileAuthenticated);
  }));
});
