import { TestBed } from '@angular/core/testing';
import { of, Subject, throwError } from 'rxjs';
import { vi } from 'vitest';
import type {
  CharacterResponse,
  CurrentLocationResponse,
  CurrentTravel,
} from '../../core/api/game-api.models';
import { GameApiService } from '../../core/api/game-api.service';
import { WorldStore } from './world.store';

const character: CharacterResponse = {
  id: 'character-id',
  name: 'Aric Duskwalker',
  level: 1,
  experience: 0,
  currentHp: 100,
  maxHp: 100,
  attack: 6,
  currentLocation: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
};

const currentLocation: CurrentLocationResponse = {
  id: 'origin-id',
  key: 'south-gate',
  name: 'Südtor',
  description: 'Der Ausgang zur Wildnis.',
  regionKey: 'ashen-fields',
  minRecommendedLevel: 1,
  maxRecommendedLevel: 1,
  dangerLevel: 1,
  isSafe: true,
  huntingEnabled: false,
  artworkPath: '/images/backgrounds/Suedtor.png',
  connections: [
    {
      targetLocation: {
        id: 'target-id',
        key: 'burned-road',
        name: 'Verbrannte Straße',
      },
      travelDurationSeconds: 10,
      danger: 'LOW',
    },
  ],
};

const travelling: CurrentTravel = {
  status: 'TRAVELLING',
  originLocation: character.currentLocation,
  targetLocation: currentLocation.connections[0].targetLocation,
  startedAt: '2026-08-18T10:00:00.000Z',
  arrivesAt: '2026-08-18T10:00:10.000Z',
};

const expiredTravelling: CurrentTravel = {
  ...travelling,
  arrivesAt: '2026-08-18T09:59:59.000Z',
};

describe('WorldStore', () => {
  let api: {
    getCharacter: ReturnType<typeof vi.fn>;
    getCurrentLocation: ReturnType<typeof vi.fn>;
    getCurrentTravel: ReturnType<typeof vi.fn>;
    startTravel: ReturnType<typeof vi.fn>;
  };
  let store: WorldStore;

  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-08-18T10:00:00.000Z'));
    api = {
      getCharacter: vi.fn(() => of(character)),
      getCurrentLocation: vi.fn(() => of(currentLocation)),
      getCurrentTravel: vi.fn(() => of({ status: 'IDLE' } satisfies CurrentTravel)),
      startTravel: vi.fn(() => of(travelling)),
    };

    TestBed.configureTestingModule({
      providers: [WorldStore, { provide: GameApiService, useValue: api }],
    });
    store = TestBed.inject(WorldStore);
  });

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

  it('loads the character, current location, and current travel', async () => {
    await store.load();

    expect(api.getCharacter).toHaveBeenCalledOnce();
    expect(api.getCurrentLocation).toHaveBeenCalledOnce();
    expect(api.getCurrentTravel).toHaveBeenCalledOnce();
    expect(store.character()).toEqual(character);
    expect(store.currentLocation()).toEqual(currentLocation);
  });

  it('starts travel with only the selected target ID', async () => {
    await store.load();
    store.selectConnection(currentLocation.connections[0]);

    await store.startTravel();

    expect(api.startTravel).toHaveBeenCalledWith('target-id');
  });

  it('derives the remaining seconds from the server arrivesAt timestamp', async () => {
    api.getCurrentTravel.mockReturnValue(of(travelling));

    await store.load();

    expect(store.remainingSeconds()).toBe(10);
  });

  it('polls the server at zero without assigning the target as the current location', async () => {
    api.getCurrentTravel
      .mockReturnValueOnce(of(travelling))
      .mockReturnValueOnce(of({ status: 'IDLE' } satisfies CurrentTravel));

    await store.load();
    const initialLocation = store.currentLocation();

    await vi.advanceTimersByTimeAsync(10_000);

    expect(api.getCurrentTravel).toHaveBeenCalledTimes(2);
    expect(store.currentLocation()).toBe(initialLocation);
    expect(api.getCharacter).toHaveBeenCalledOnce();
    expect(api.getCurrentLocation).toHaveBeenCalledOnce();
  });

  it('makes one immediate poll without a countdown interval for an already expired arrival', async () => {
    const pendingTravel = new Subject<CurrentTravel>();
    api.getCurrentTravel.mockReturnValueOnce(of(expiredTravelling)).mockReturnValue(pendingTravel);

    await store.load();
    await vi.advanceTimersByTimeAsync(1_000);

    expect(api.getCurrentTravel).toHaveBeenCalledTimes(2);

    pendingTravel.next({ status: 'IDLE' });
    pendingTravel.complete();
  });

  it('does not reload character or location when the authoritative poll remains travelling', async () => {
    api.getCurrentTravel
      .mockReturnValueOnce(of(expiredTravelling))
      .mockReturnValueOnce(of(expiredTravelling));

    await store.load();
    await vi.advanceTimersByTimeAsync(0);

    expect(api.getCharacter).toHaveBeenCalledOnce();
    expect(api.getCurrentLocation).toHaveBeenCalledOnce();
  });

  it('retries a transient travel poll failure after one second and recovers from the API', async () => {
    api.getCurrentTravel
      .mockReturnValueOnce(of(expiredTravelling))
      .mockReturnValueOnce(throwError(() => new Error('Temporary failure')))
      .mockReturnValueOnce(of({ status: 'IDLE' } satisfies CurrentTravel));

    await store.load();
    await vi.advanceTimersByTimeAsync(999);

    expect(api.getCurrentTravel).toHaveBeenCalledTimes(2);
    expect(store.error()).toBe('Temporary failure');

    await vi.advanceTimersByTimeAsync(1);

    expect(api.getCurrentTravel).toHaveBeenCalledTimes(3);
    expect(store.currentTravel()).toEqual({ status: 'IDLE' });
    expect(store.error()).toBeNull();
  });

  it('ignores a late poll response after the store is destroyed', async () => {
    const pendingTravel = new Subject<CurrentTravel>();
    api.getCurrentTravel
      .mockReturnValueOnce(of(expiredTravelling))
      .mockReturnValueOnce(pendingTravel);

    await store.load();
    store.ngOnDestroy();
    pendingTravel.next({ status: 'COMPLETED', targetLocation: travelling.targetLocation });
    pendingTravel.complete();
    await Promise.resolve();

    expect(api.getCharacter).toHaveBeenCalledOnce();
    expect(api.getCurrentLocation).toHaveBeenCalledOnce();
    await vi.advanceTimersByTimeAsync(2_000);
    expect(api.getCurrentTravel).toHaveBeenCalledTimes(2);
  });

  it('reloads character and location only after the server reports completion', async () => {
    api.getCurrentTravel
      .mockReturnValueOnce(of(travelling))
      .mockReturnValueOnce(of({ status: 'COMPLETED', targetLocation: travelling.targetLocation }));

    await store.load();
    await vi.advanceTimersByTimeAsync(10_000);

    expect(api.getCharacter).toHaveBeenCalledTimes(2);
    expect(api.getCurrentLocation).toHaveBeenCalledTimes(2);
  });
});
