// App.characterization.test.js
// Characterize App -> Firebase calls. Lock path + payload shape per action.
// Mock SDK, render AdminView, fire action, assert recorded calls.
// Purpose: refactor (path-shape rewrite) must not change these calls.

import React from 'react';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { getCalls, MOCK_DB } from '../__mocks__/firebase/_mock-db';
import { renderApp, createCampaignViaUI, selectCampaignByName, setupReady, startCombatViaUI, addMonsterViaUI } from './testHelpers';

function findCall(fn, pathSub) {
  return getCalls().find(c => c.fn === fn && (pathSub ? c.path.includes(pathSub) : true));
}
function findCalls(fn, pathSub) {
  return getCalls().filter(c => c.fn === fn && (pathSub ? c.path.includes(pathSub) : true));
}

// ============================================================================
// CAMPAIGN GROUP
// ============================================================================

describe('Campaign -> Firebase', () => {
  test('createCampaign: setDoc with campaign path + payload', async () => {
    await renderApp();
    const id = await createCampaignViaUI('Alpha');
    const call = findCall('setDoc', '/campaigns/');
    expect(call.path).toMatch(/campaigns\/.+$/);
    expect(call.data).toMatchObject({
      name: 'Alpha',
      playerDisplayBackgroundUrl: '',
      players: [],
    });
    expect(call.data).toHaveProperty('ownerId');
    expect(call.data).toHaveProperty('createdAt');
  });

  test('createCampaign: path includes APP_ID namespace', async () => {
    await renderApp();
    await createCampaignViaUI('NS Test');
    const call = findCall('setDoc', '/campaigns/');
    expect(call.path).toContain('artifacts/');
    expect(call.path).toContain('/public/data/');
  });

  test('createCampaign: optional background URL stored', async () => {
    await renderApp();
    fireEvent.click(screen.getByRole('button', { name: /Create Campaign/i }));
    await waitFor(() => screen.getByLabelText(/Campaign Name/i));
    fireEvent.change(screen.getByLabelText(/Campaign Name/i), { target: { value: 'With BG' } });
    fireEvent.change(screen.getByLabelText(/Background URL/i), { target: { value: 'https://img.test/bg.png' } });
    fireEvent.click(screen.getByRole('button', { name: /^Create$/i }));
    await waitFor(() => findCall('setDoc', '/campaigns/'));
    const call = findCall('setDoc', '/campaigns/');
    expect(call.data.playerDisplayBackgroundUrl).toBe('https://img.test/bg.png');
  });

  test('addCharacter: updateDoc on campaign doc, players array grows', async () => {
    await renderApp();
    const cid = await createCampaignViaUI('Roster');
    await selectCampaignByName('Roster');

    // CharacterManager form
    fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: 'Brog' } });
    fireEvent.change(screen.getByLabelText(/^HP$/i), { target: { value: '25' } });
    fireEvent.change(screen.getByLabelText(/Init Mod/i), { target: { value: '3' } });
    fireEvent.click(screen.getByRole('button', { name: /Add Character/i }));

    await waitFor(() => findCall('updateDoc', '/campaigns/'));
    const call = findCall('updateDoc', `/campaigns/${cid}`);
    expect(call.data.players).toHaveLength(1);
    expect(call.data.players[0]).toMatchObject({
      name: 'Brog',
      defaultMaxHp: 25,
      defaultInitMod: 3,
    });
    expect(call.data.players[0]).toHaveProperty('id');
  });

  test('updateCharacter: updateDoc with updated players array', async () => {
    await renderApp();
    const cid = await createCampaignViaUI('EditRoster');
    await selectCampaignByName('EditRoster');

    // add one first
    fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: 'Old Name' } });
    fireEvent.click(screen.getByRole('button', { name: /Add Character/i }));
    await waitFor(() => findCall('updateDoc', '/campaigns/'));

    // click edit
    const editBtn = await screen.findByRole('button', { name: /Edit character/i });
    fireEvent.click(editBtn);
    await waitFor(() => screen.getByDisplayValue('Old Name'));
    fireEvent.change(screen.getByDisplayValue('Old Name'), { target: { value: 'New Name' } });
    // Save button is icon-only (no text); submit its form.
    const form = screen.getByDisplayValue('New Name').closest('form');
    fireEvent.submit(form);

    await waitFor(() => {
      const calls = findCalls('updateDoc', `/campaigns/${cid}`);
      const last = calls[calls.length - 1];
      expect(last.data.players[0].name).toBe('New Name');
    });
  });

  test('deleteCharacter: updateDoc with character removed', async () => {
    await renderApp();
    const cid = await createCampaignViaUI('DeleteRoster');
    await selectCampaignByName('DeleteRoster');

    fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: 'Gone' } });
    fireEvent.click(screen.getByRole('button', { name: /Add Character/i }));
    await waitFor(() => findCall('updateDoc', '/campaigns/'));

    const delBtn = await screen.findByRole('button', { name: /Delete character/i });
    fireEvent.click(delBtn);
    // confirmation modal
    fireEvent.click(screen.getByRole('button', { name: /Confirm/i }));

    await waitFor(() => {
      const calls = findCalls('updateDoc', `/campaigns/${cid}`);
      const last = calls[calls.length - 1];
      expect(last.data.players).toHaveLength(0);
    });
  });

  test('deleteCampaign: deletes encounters batch + campaign doc + activeDisplay null', async () => {
    await renderApp();
    const cid = await createCampaignViaUI('Doomed');
    await selectCampaignByName('Doomed');

    // campaign card delete button has no aria-label; find trash by text via grid
    const allDeletes = screen.getAllByText(/Delete/i);
    // campaign card Delete is in card grid, last one rendered
    fireEvent.click(allDeletes[allDeletes.length - 1]);
    fireEvent.click(await screen.findByRole('button', { name: /Confirm/i }));

    await waitFor(() => findCall('deleteDoc', `/campaigns/${cid}`));
    const delCall = findCall('deleteDoc', `/campaigns/${cid}`);
    expect(delCall).toBeDefined();
  });

  test('deleteCampaign: cascade-deletes logs for campaign encounters (BUG)', async () => {
    const { setupReady, startCombatViaUI } = require('./testHelpers');
    await setupReady('Doomed', 'Enc');
    await addMonsterViaUI('Gob', 5, 2);
    await startCombatViaUI();
    // log written on start
    await waitFor(() => {
      const logWrites = findCalls('addDoc').filter(c => c.path.includes('/logs'));
      expect(logWrites.length).toBeGreaterThan(0);
    });
    const cid = Object.keys(MOCK_DB.collection('campaigns').reduce((m,c)=>(m[c.id]=c,m),{}))[0];

    // delete campaign
    const allDeletes = screen.getAllByText(/Delete/i);
    fireEvent.click(allDeletes[allDeletes.length - 1]);
    fireEvent.click(await screen.findByRole('button', { name: /Confirm/i }));
    await waitFor(() => findCall('deleteDoc', `/campaigns/${cid}`));

    // logs MUST be deleted via batch or deleteDoc on each log path
    const logDeletes = getCalls().filter(c =>
      (c.fn === 'deleteDoc' && c.path.includes('/logs/')) ||
      (c.fn === 'batch.delete' && c.path.includes('/logs/'))
    );
    expect(logDeletes.length).toBeGreaterThan(0);
  });

  test('deleteEncounter: cascade-deletes logs for encounter (BUG)', async () => {
    const { setupReady, startCombatViaUI } = require('./testHelpers');
    await setupReady('Camp', 'DoomedEnc');
    await addMonsterViaUI('Gob', 5, 2);
    await startCombatViaUI();
    await waitFor(() => {
      expect(findCalls('addDoc').filter(c => c.path.includes('/logs')).length).toBeGreaterThan(0);
    });

    // delete encounter
    const allDeletes = screen.getAllByText(/Delete/i);
    fireEvent.click(allDeletes[0]);
    fireEvent.click(await screen.findByRole('button', { name: /Confirm/i }));

    await waitFor(() => {
      const logDeletes = getCalls().filter(c =>
        (c.fn === 'deleteDoc' && c.path.includes('/logs/')) ||
        (c.fn === 'batch.delete' && c.path.includes('/logs/'))
      );
      expect(logDeletes.length).toBeGreaterThan(0);
    });
  });
});
