import { ApplicationRef, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter';
import { createDraftSync, DraftSnapshot } from './draft-sync';

function setup(adapter: Partial<ApplicationsAdapter>) {
  const navigate = vi.fn().mockResolvedValue(true);
  TestBed.configureTestingModule({
    providers: [
      { provide: ApplicationsAdapter, useValue: adapter },
      { provide: Router, useValue: { navigate } },
      { provide: ActivatedRoute, useValue: { snapshot: { queryParamMap: { get: () => null } } } },
    ],
  });
  const snap = signal<DraftSnapshot | null>(null);
  const onResume = vi.fn();
  const draftSync = TestBed.runInInjectionContext(() =>
    createDraftSync({
      type: 'registratie',
      snapshot: () => snap(),
      onResume,
      enabled: () => true,
    }),
  );
  TestBed.inject(ApplicationRef).tick(); // flush the effect's initial run
  return { draftSync, snap, navigate, onResume };
}

const tick = () => TestBed.inject(ApplicationRef).tick();

describe('createDraftSync', () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  it('coalesces rapid snapshot changes into ONE debounced sync of the latest value', async () => {
    const create = vi.fn().mockResolvedValue('a1');
    const syncDraft = vi.fn().mockResolvedValue(undefined);
    const { snap } = setup({ create, syncDraft });

    snap.set({ draft: { step: 1 }, stepIndex: 0, stepCount: 3, documentIds: [] });
    tick();
    snap.set({ draft: { step: 1, x: 'a' }, stepIndex: 0, stepCount: 3, documentIds: [] });
    tick();
    snap.set({ draft: { step: 1, x: 'ab' }, stepIndex: 0, stepCount: 3, documentIds: [] });
    tick();

    // still inside the 600ms debounce window — nothing has synced yet
    await vi.advanceTimersByTimeAsync(500);
    expect(syncDraft).not.toHaveBeenCalled();

    await vi.advanceTimersByTimeAsync(200);
    expect(create).toHaveBeenCalledTimes(1); // one Concept created, not three
    expect(syncDraft).toHaveBeenCalledTimes(1); // one sync, not three
    expect(syncDraft).toHaveBeenCalledWith(
      'a1',
      expect.objectContaining({ draft: { step: 1, x: 'ab' } }), // the LAST snapshot wins
    );
  });

  it('a trailing change after the debounce fires schedules its own sync', async () => {
    const create = vi.fn().mockResolvedValue('a1');
    const syncDraft = vi.fn().mockResolvedValue(undefined);
    const { snap } = setup({ create, syncDraft });

    snap.set({ draft: { step: 1 }, stepIndex: 0, stepCount: 3, documentIds: [] });
    tick();
    await vi.advanceTimersByTimeAsync(600);
    expect(syncDraft).toHaveBeenCalledTimes(1);

    snap.set({ draft: { step: 2 }, stepIndex: 1, stepCount: 3, documentIds: [] });
    tick();
    await vi.advanceTimersByTimeAsync(600);
    expect(syncDraft).toHaveBeenCalledTimes(2);
    expect(syncDraft).toHaveBeenLastCalledWith('a1', expect.objectContaining({ stepIndex: 1 }));
  });

  describe('submit', () => {
    it('resolves ok with the server response on success', async () => {
      const create = vi.fn().mockResolvedValue('a1');
      const submit = vi.fn().mockResolvedValue({ id: 'a1', autoApprovable: true });
      const { draftSync } = setup({ create, submit });

      const r = await draftSync.submit({});
      expect(r).toEqual({ ok: true, value: { id: 'a1', autoApprovable: true } });
      expect(submit).toHaveBeenCalledWith('a1', {});
    });

    it('folds a rejected submit into a Result error, never throwing', async () => {
      const create = vi.fn().mockResolvedValue('a1');
      const submit = vi.fn().mockRejectedValue(new Error('boom'));
      const { draftSync } = setup({ create, submit });

      const r = await draftSync.submit({});
      expect(r.ok).toBe(false);
    });
  });
});
