/**
 * Bug Condition Exploration Property Test:
 *   Compliance List Stale After Sidebar Edit
 *
 * Spec:  .kiro/specs/compliance-list-stale-after-sidebar-edit/ (bugfix)
 * Issue: #23 — "[Bug] Update Res Date/Remed Plan in list after updating in sidebar"
 *        http://steam-gitlab.charterlab.com/steam/cve-dashboard/-/issues/23
 *
 * BUG CONDITION (bugfix.md Current Behavior 1.1–1.3):
 *   isBugCondition(input) = a successful PATCH /api/compliance/items/:hostname/metadata
 *   occurred from the sidebar (ComplianceDetailPanel). Under this condition the parent
 *   CompliancePage list is never re-fetched: handleSaveMetadata() does not notify the
 *   parent (1.1), onClose only clears selectedHost (1.2), and the row keeps the stale
 *   Resolution Date / Remediation Plan held in the parent `devices` state (1.3).
 *
 * THIS TEST IS EXPECTED TO FAIL ON UNFIXED CODE — failure confirms the bug.
 *   The property encodes the expected behavior (bugfix.md 2.1, 2.2): for any saved
 *   metadata value, after a successful sidebar save the parent list row for that
 *   hostname displays the saved value WITHOUT a manual filter/team/tab change or a
 *   manual refresh click, because a parent refresh callback re-issues fetchDevices.
 *   On unfixed code no parent callback fires, so the list GET is never re-issued after
 *   the PATCH and the row keeps showing the stale value ("—").
 *
 * Mirrors the tagging convention of
 *   backend/__tests__/compliance-remediation-display-fix.exploration.property.test.js
 *
 * **Validates: Requirements 1.1, 1.2, 1.3, 2.1, 2.2**
 */
import React from 'react';
import { render, screen, fireEvent, waitFor, within, cleanup } from '@testing-library/react';
import fc from 'fast-check';

// --- Mocks (hoisted by babel-jest above the CompliancePage import) ---

// Stub auth so CompliancePage renders the STEAM team with write access.
jest.mock('../../../contexts/AuthContext', () => ({
  useAuth: () => ({
    canWrite: () => true,
    isAdmin: () => false,
    getAvailableTeams: () => ['STEAM'],
    adminScope: null,
  }),
}));

// The historical charts panel is irrelevant to the list-staleness bug and pulls
// in recharts; stub it so the test stays focused and fast across property runs.
jest.mock('../ComplianceChartsPanel', () => () => null);

import CompliancePage from '../CompliancePage';

const HOSTNAME = 'HOST-001';
const METRIC_ID = '2.3.6i';

// --- Fixtures --------------------------------------------------------------

function jsonResponse(body, ok = true) {
  return Promise.resolve({ ok, json: async () => body });
}

function makeListDevice({ resolution_date, remediation_plan }) {
  return {
    hostname: HOSTNAME,
    ip_address: '10.0.0.1',
    device_type: 'Switch',
    failing_metrics: [{ metric_id: METRIC_ID, category: 'Vulnerability Management' }],
    resolution_date,
    remediation_plan,
    seen_count: 3,
    has_notes: false,
  };
}

function makeDetail() {
  return {
    hostname: HOSTNAME,
    ip_address: '10.0.0.1',
    device_type: 'Switch',
    team: 'STEAM',
    metrics: [
      {
        metric_id: METRIC_ID,
        category: 'Vulnerability Management',
        status: 'active',
        metric_desc: 'Outbound encryption required on all endpoints',
        resolution_date: null,
        remediation_plan: null,
        seen_count: 3,
        first_seen: '2025-01-01',
        resolved_on: null,
        extra: {},
      },
    ],
    history: [],
    notes: [],
  };
}

/**
 * Install a URL-routing global.fetch.
 *
 * The list device starts stale (resolution_date / remediation_plan = null). Only
 * AFTER a successful metadata PATCH does the list endpoint return the saved values —
 * modelling the backend, which already persists and returns the new metadata. Thus a
 * post-save re-fetch is observable in the row, while on unfixed code (no re-fetch) the
 * row keeps showing the stale value.
 */
function installFetchMock(savedResolutionDate, savedRemediationPlan) {
  const state = { patchOccurred: false, patchCalls: 0, listCalls: 0, listCallsAfterPatch: 0 };

  global.fetch = jest.fn((url, options = {}) => {
    const method = (options.method || 'GET').toUpperCase();

    // PATCH /compliance/items/:hostname/metadata → success
    if (method === 'PATCH' && url.includes('/compliance/items/') && url.includes('/metadata')) {
      state.patchCalls++;
      state.patchOccurred = true;
      return jsonResponse({ ok: true });
    }

    // GET /compliance/summary?team=STEAM → minimal valid summary
    if (url.includes('/compliance/summary')) {
      return jsonResponse({ entries: [], overall_scores: {}, upload: { report_date: '2025-01-01' } });
    }

    // GET /compliance/items?team=STEAM&status=active → device list (query string)
    if (url.includes('/compliance/items?')) {
      state.listCalls++;
      if (state.patchOccurred) state.listCallsAfterPatch++;
      const device = state.patchOccurred
        ? makeListDevice({ resolution_date: savedResolutionDate, remediation_plan: savedRemediationPlan })
        : makeListDevice({ resolution_date: null, remediation_plan: null });
      return jsonResponse({ devices: [device] });
    }

    // GET /compliance/items/:hostname → detail (one active metric)
    if (url.includes('/compliance/items/')) {
      return jsonResponse(makeDetail());
    }

    // Any other URL (charts panel is mocked out) → safe empty payload
    return jsonResponse({});
  });

  return state;
}

// --- Generators ------------------------------------------------------------

/**
 * YYYY-MM-DD strings built from integer tuples. Do NOT call toISOString on shrunk
 * values (mirrors the predecessor exploration test's date-generator pattern).
 */
const arbResolutionDate = fc
  .tuple(
    fc.integer({ min: 2026, max: 2030 }),
    fc.integer({ min: 1, max: 12 }),
    fc.integer({ min: 1, max: 28 })
  )
  .map(([y, m, d]) => `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`);

/** Non-empty, trimmed remediation plan strings, length-bounded 1–200. */
const arbRemediationPlan = fc
  .string({ minLength: 1, maxLength: 200 })
  .filter((s) => s.trim().length > 0);

// --- Setup / teardown ------------------------------------------------------

beforeEach(() => {
  process.env.REACT_APP_API_BASE = 'http://localhost:3001/api';
});

afterEach(() => {
  cleanup();
  jest.restoreAllMocks();
});

// --- Property Test ---------------------------------------------------------

describe('Bug Condition Exploration: list row stays stale after sidebar metadata save', () => {
  it(
    'Property 1: for any saved metadata, the parent list row reflects the saved Resolution Date after a successful sidebar save (no manual refresh)',
    async () => {
      await fc.assert(
        fc.asyncProperty(arbResolutionDate, arbRemediationPlan, async (savedResolutionDate, savedRemediationPlan) => {
          const state = installFetchMock(savedResolutionDate, savedRemediationPlan);

          try {
            const { container } = render(<CompliancePage onNavigate={() => {}} />);

            // Wait for the (stale) device row, then capture the row element.
            const hostCell = await screen.findByText(HOSTNAME);
            const row = hostCell.parentElement;

            // Pre-condition: the stale row does not yet show the to-be-saved date.
            expect(within(row).queryByText(savedResolutionDate)).toBeNull();

            // Open the sidebar detail panel.
            fireEvent.click(row);

            // Wait for the panel's editable Resolution Date input to render.
            const dateInput = await waitFor(() => {
              const el = container.querySelector('input[type="date"]');
              if (!el) throw new Error('resolution date input not ready');
              return el;
            });
            const planInput = screen.getByPlaceholderText(/Describe the remediation plan/i);

            // Enter the generated metadata values and click Save.
            fireEvent.change(dateInput, { target: { value: savedResolutionDate } });
            fireEvent.change(planInput, { target: { value: savedRemediationPlan } });
            fireEvent.click(screen.getByRole('button', { name: /save/i }));

            // The PATCH is issued on both fixed and unfixed code.
            await waitFor(() => expect(state.patchCalls).toBeGreaterThan(0));

            // PROPERTY (expected behavior, bugfix.md 2.1/2.2): the parent list row
            // reflects the saved Resolution Date without a manual refresh. This holds
            // only if a parent refresh callback re-issued fetchDevices after the save.
            await waitFor(
              () => {
                expect(within(row).getByText(savedResolutionDate)).toBeInTheDocument();
              },
              { timeout: 2000 }
            );

            // Supporting: the list endpoint was re-issued after the save.
            expect(state.listCallsAfterPatch).toBeGreaterThan(0);
          } finally {
            cleanup();
          }
        }),
        { numRuns: 10, endOnFailure: true }
      );
    },
    60000
  );
});
