import {
  Injector,
  runInInjectionContext,
  signal
} from '@angular/core';
import { Store } from '@ngrx/store';
import type {
  Message,
  Room,
  User
} from '../../../../shared-kernel';
import { NotificationAudioService } from '../../../../core/services/notification-audio.service';
import { TimeSyncService } from '../../../../core/services/time-sync.service';
import { DatabaseService } from '../../../../infrastructure/persistence';
import {
  selectActiveChannelId,
  selectCurrentRoom,
  selectSavedRooms
} from '../../../../store/rooms/rooms.selectors';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import { DesktopNotificationService } from '../../infrastructure/services/desktop-notification.service';
import { NotificationSettingsStorageService } from '../../infrastructure/services/notification-settings-storage.service';
import { createDefaultNotificationSettings, type NotificationsSettings } from '../../domain/models/notification.model';
import { initializeAppI18nForTests, provideAppI18nForTests } from '../../../../core/i18n/app-i18n.testing';
import { NotificationsService } from './notifications.service';

const alice: User = {
  id: 'alice',
  oderId: 'alice',
  username: 'alice',
  displayName: 'Alice',
  status: 'online',
  role: 'member',
  joinedAt: 1
};

describe('NotificationsService', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(1_000);
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('keeps channel read markers when startup room metadata has no channels yet', async () => {
    const roomWithoutChannels = createRoom({ channels: [] });
    const context = createServiceContext({
      currentUser: alice,
      savedRooms: [roomWithoutChannels],
      settings: {
        ...createDefaultNotificationSettings(),
        roomBaselines: { 'room-1': 10 },
        lastReadByChannel: {
          'room-1': {
            'channel-1': 100
          }
        }
      }
    });

    await context.service.initialize();

    expect(context.service.settings().lastReadByChannel['room-1']?.['channel-1']).toBe(100);
  });

  it('uses the server-adjusted clock when marking the active channel read', async () => {
    const room = createRoom();
    const context = createServiceContext({
      activeChannelId: 'channel-1',
      currentRoom: room,
      currentUser: alice,
      savedRooms: [room],
      settings: {
        ...createDefaultNotificationSettings(),
        roomBaselines: { 'room-1': 0 }
      },
      timeNow: 3_000
    });

    await context.service.initialize();

    expect(context.service.settings().lastReadByChannel['room-1']?.['channel-1']).toBe(3_000);
  });

  it('keeps the visible active channel read when startup message loads recompute unread', async () => {
    const room = createRoom();
    const context = createServiceContext({
      activeChannelId: 'channel-1',
      currentRoom: room,
      currentUser: alice,
      savedRooms: [room],
      settings: {
        ...createDefaultNotificationSettings(),
        roomBaselines: { 'room-1': 0 }
      },
      timeNow: 100
    });

    await context.service.initialize();

    context.service.refreshRoomUnreadFromMessages('room-1', [createMessage({ channelId: 'channel-1', senderId: 'bob', timestamp: 200 })]);

    expect(context.service.channelUnreadCount('room-1', 'channel-1')).toBe(0);
    expect(context.service.roomUnreadCount('room-1')).toBe(0);
  });

  it('does not compute unread from messages before it is initialised', () => {
    const room = createRoom();
    const context = createServiceContext({
      currentUser: alice,
      savedRooms: [room],
      settings: createDefaultNotificationSettings()
    });

    context.service.refreshRoomUnreadFromMessages('room-1', [createMessage({ channelId: 'channel-1', senderId: 'bob', timestamp: 5_000 })]);

    expect(context.service.roomUnreadCount('room-1')).toBe(0);
    expect(context.service.channelUnreadCount('room-1', 'channel-1')).toBe(0);
  });

  it('hydrates unread counts from persisted read markers on initialize', async () => {
    const room = createRoom();
    const context = createServiceContext({
      activeChannelId: 'other',
      currentRoom: null,
      currentUser: alice,
      dbMessages: [createMessage({ channelId: 'channel-1', senderId: 'bob', timestamp: 5_000 })],
      savedRooms: [room],
      settings: {
        ...createDefaultNotificationSettings(),
        roomBaselines: { 'room-1': 0 }
      },
      timeNow: 10_000
    });

    await context.service.initialize();

    expect(context.service.channelUnreadCount('room-1', 'channel-1')).toBe(1);
    expect(context.service.roomUnreadCount('room-1')).toBe(1);
  });
});

interface ServiceContextOptions {
  activeChannelId?: string | null;
  currentRoom?: Room | null;
  currentUser: User | null;
  dbMessages?: Message[];
  savedRooms: Room[];
  settings: NotificationsSettings;
  timeNow?: number;
}

interface ServiceContext {
  service: NotificationsService;
}

function createServiceContext(options: ServiceContextOptions): ServiceContext {
  const currentUser = signal<User | null>(options.currentUser);
  const savedRooms = signal<Room[]>(options.savedRooms);
  const currentRoom = signal<Room | null>(options.currentRoom ?? null);
  const activeChannelId = signal<string | null>(options.activeChannelId ?? null);

  let storedSettings = options.settings;

  const injector = Injector.create({
    providers: [
      ...provideAppI18nForTests(),
      {
        provide: DatabaseService,
        useValue: {
          getMessagesSince: vi.fn(async (): Promise<Message[]> => options.dbMessages ?? [])
        }
      },
      {
        provide: DesktopNotificationService,
        useValue: {
          clearAttention: vi.fn(),
          onWindowStateChanged: vi.fn(() => () => undefined),
          requestAttention: vi.fn(),
          showNotification: vi.fn(async () => undefined)
        }
      },
      {
        provide: NotificationAudioService,
        useValue: {
          play: vi.fn()
        }
      },
      {
        provide: TimeSyncService,
        useValue: {
          now: vi.fn(() => options.timeNow ?? Date.now())
        }
      },
      {
        provide: NotificationSettingsStorageService,
        useValue: {
          load: vi.fn(() => storedSettings),
          save: vi.fn((settings: NotificationsSettings) => {
            storedSettings = settings;
          })
        }
      },
      {
        provide: Store,
        useValue: {
          selectSignal: vi.fn((selector: unknown) => {
            if (selector === selectCurrentRoom) {
              return currentRoom;
            }

            if (selector === selectActiveChannelId) {
              return activeChannelId;
            }

            if (selector === selectSavedRooms) {
              return savedRooms;
            }

            if (selector === selectCurrentUser) {
              return currentUser;
            }

            throw new Error('Unexpected selector requested by NotificationsService test.');
          })
        }
      }
    ]
  });

  initializeAppI18nForTests(injector);

  return {
    service: runInInjectionContext(injector, () => new NotificationsService())
  };
}

function createRoom(overrides: Partial<Room> = {}): Room {
  return {
    id: 'room-1',
    name: 'Room 1',
    description: '',
    channels: [
      {
        id: 'channel-1',
        name: 'general',
        type: 'text'
      }
    ],
    members: [],
    roles: [],
    ...overrides
  } as Room;
}

function createMessage(overrides: Partial<Message> = {}): Message {
  return {
    id: 'message-1',
    roomId: 'room-1',
    channelId: 'channel-1',
    senderId: 'bob',
    senderName: 'Bob',
    content: 'hello',
    timestamp: 1,
    reactions: [],
    isDeleted: false,
    ...overrides
  };
}
