/**
 * Render and interaction tests for the per-metric estimated-resolution-date
 * line in the asset sidebar (ComplianceDetailPanel.js / MetricRow).
 *
 * Feature: compliance-metric-estimated-resolution-date
 * Spec: .kiro/specs/compliance-metric-estimated-resolution-date
 *
 * Covers tasks 3.2 (placement, labels, placeholders), 3.3 (resolved
 * suppression, read-only structure, role-independence, existing editor
 * preserved), and 3.4 (existing save round-trip).
 *
 * Requirements covered: 1.1, 1.2, 1.4, 1.5, 1.6, 2.1, 3.1, 3.4, 4.1, 4.2,
 * 4.3, 4.4, 4.5, 5.1, 5.2, 5.3, 5.4.
 *
 * The component fetches the asset detail via GET
 * `${API_BASE}/compliance/items/:hostname` (credentials included) and saves
 * metadata via PATCH `${API_BASE}/compliance/items/:hostname/metadata`, so
 * global.fetch is mocked to serve the asset detail JSON and PATCH responses.
 */
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ComplianceDetailPanel from '../ComplianceDetailPanel';
import {
  RESOLUTION_DATE_LABEL,
  NO_DATE_PLACEHOLDER,
  INVALID_DATE_PLACEHOLDER,
} from '../../../utils/resolutionDate';

const HOSTNAME = 'host-1.example.com';

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

function makeMetric(overrides = {}) {
  return {
    metric_id: '2.3.6i',
    category: 'Vulnerability Management',
    status: 'active',
    metric_desc: 'Outbound encryption required on all endpoints',
    resolution_date: null,
    remediation_plan: null,
    seen_count: 1,
    first_seen: '2025-01-01',
    resolved_on: null,
    extra: {},
    ...overrides,
  };
}

function makeDetail(metrics, overrides = {}) {
  return {
    hostname: HOSTNAME,
    ip_address: '10.0.0.1',
    device_type: 'server',
    team: 'STEAM',
    metrics,
    history: [],
    notes: [],
    ...overrides,
  };
}

const jsonResponse = (body, ok = true) => ({ ok, json: async () => body });

/**
 * Mock global.fetch. GET requests to the detail endpoint are served from a
 * queue of detail objects (the last one is reused once the queue drains, so
 * the initial load and any re-fetch can each return a distinct snapshot).
 * PATCH requests to the metadata endpoint return the configured response.
 */
function mockFetch({ details, patchOk = true, patchBody = {} }) {
  const queue = [...details];
  let last = details[details.length - 1];
  global.fetch = jest.fn((url, options = {}) => {
    const method = (options.method || 'GET').toUpperCase();
    if (method === 'PATCH') {
      return Promise.resolve(jsonResponse(patchBody, patchOk));
    }
    // GET detail (fetchDetail)
    const next = queue.length > 0 ? queue.shift() : last;
    last = next;
    return Promise.resolve(jsonResponse(next));
  });
}

// ---------------------------------------------------------------------------
// DOM query helpers
// ---------------------------------------------------------------------------

// The estimated-resolution date line renders as:
//   <div>
//     <svg .../>                      (Calendar icon)
//     <span>{RESOLUTION_DATE_LABEL}</span>
//     <span>{value | placeholder}</span>
//   </div>
// We locate each line by its label span, which contains exactly the label text.

function getDateLineLabels(container) {
  return Array.from(container.querySelectorAll('span')).filter(
    (s) => s.textContent === RESOLUTION_DATE_LABEL
  );
}

function getDateLineValueTexts(container) {
  return getDateLineLabels(container).map((label) =>
    label.nextElementSibling ? label.nextElementSibling.textContent : null
  );
}

async function renderAndLoad(detail, props = {}) {
  const utils = render(
    <ComplianceDetailPanel
      hostname={HOSTNAME}
      onClose={() => {}}
      onNoteAdded={() => {}}
      {...props}
    />
  );
  // Wait for the async detail load to complete (a metric description renders).
  await screen.findByText(detail.metrics[0].metric_desc);
  return utils;
}

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

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

// ===========================================================================
// Task 3.2 — Render tests for placement, labels, and placeholders
// ===========================================================================

describe('Task 3.2 — placement, labels, and placeholders', () => {
  test('placement (Req 1.2): estimated-resolution element precedes the metric description', async () => {
    const detail = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    const labelEl = screen.getByText(RESOLUTION_DATE_LABEL);
    const descEl = screen.getByText(detail.metrics[0].metric_desc);

    // descEl must come AFTER labelEl in document order.
    expect(
      labelEl.compareDocumentPosition(descEl) &
        Node.DOCUMENT_POSITION_FOLLOWING
    ).toBeTruthy();

    // Sanity: there is exactly one date line for the single active metric.
    expect(getDateLineLabels(container)).toHaveLength(1);
  });

  test('label presence (Req 1.5): RESOLUTION_DATE_LABEL appears adjacent to the value', async () => {
    const detail = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    const labels = getDateLineLabels(container);
    expect(labels).toHaveLength(1);
    // The value span is the immediate next sibling of the label span.
    expect(labels[0].nextElementSibling).not.toBeNull();
    expect(labels[0].nextElementSibling.textContent).toBe('2026-07-01');
  });

  test('set value (Req 1.1, 1.4): an active row with 2026-07-01 renders 2026-07-01', async () => {
    const detail = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    expect(getDateLineValueTexts(container)).toEqual(['2026-07-01']);
  });

  test('no-date placeholder (Req 2.1, 4.5): null/empty/whitespace render NO_DATE_PLACEHOLDER and keep the description', async () => {
    const metrics = [
      makeMetric({
        metric_id: '2.3.6i',
        metric_desc: 'Metric with null resolution date',
        resolution_date: null,
      }),
      makeMetric({
        metric_id: '2.3.8i',
        metric_desc: 'Metric with empty resolution date',
        resolution_date: '',
      }),
      makeMetric({
        metric_id: 'Vulns_Aging',
        metric_desc: 'Metric with whitespace resolution date',
        resolution_date: '   ',
      }),
    ];
    const detail = makeDetail(metrics);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    // All three active rows show the no-date placeholder.
    expect(getDateLineValueTexts(container)).toEqual([
      NO_DATE_PLACEHOLDER,
      NO_DATE_PLACEHOLDER,
      NO_DATE_PLACEHOLDER,
    ]);

    // Each metric's description still renders.
    for (const m of metrics) {
      expect(screen.getByText(m.metric_desc)).toBeInTheDocument();
    }
  });

  test('invalid placeholder (Req 1.6): malformed date renders INVALID_DATE_PLACEHOLDER and keeps the description', async () => {
    const detail = makeDetail([
      makeMetric({
        metric_desc: 'Metric with a malformed resolution date',
        resolution_date: '2026-13-99',
      }),
    ]);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    expect(getDateLineValueTexts(container)).toEqual([INVALID_DATE_PLACEHOLDER]);
    expect(
      screen.getByText('Metric with a malformed resolution date')
    ).toBeInTheDocument();
  });
});

// ===========================================================================
// Task 3.3 — resolved suppression, read-only structure, role-independence
// ===========================================================================

describe('Task 3.3 — suppression, read-only structure, role-independence', () => {
  test('resolved suppression (Req 3.1, 3.4): a resolved metric with a populated date renders no estimated-resolution line', async () => {
    const detail = makeDetail([
      makeMetric({
        metric_id: '2.3.6i',
        status: 'resolved',
        metric_desc: 'Resolved metric with a populated resolution date',
        resolution_date: '2026-07-01',
        resolved_on: '2026-06-15',
      }),
    ]);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    // No estimated-resolution line anywhere for a resolved-only asset.
    expect(getDateLineLabels(container)).toHaveLength(0);
    expect(screen.queryByText(RESOLUTION_DATE_LABEL)).toBeNull();
  });

  test('mixed list (Req 3.4): the estimated-resolution line appears only within active rows', async () => {
    const metrics = [
      makeMetric({
        metric_id: '2.3.6i',
        status: 'active',
        metric_desc: 'Active metric one',
        resolution_date: '2026-07-01',
      }),
      makeMetric({
        metric_id: '2.3.8i',
        status: 'active',
        metric_desc: 'Active metric two',
        resolution_date: '2026-09-30',
      }),
      makeMetric({
        metric_id: 'Vulns_Aging',
        status: 'resolved',
        metric_desc: 'Resolved metric',
        resolution_date: '2026-01-15',
        resolved_on: '2026-01-10',
      }),
    ];
    const detail = makeDetail(metrics);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    // Exactly two date lines (one per active metric), each its own value.
    expect(getDateLineValueTexts(container)).toEqual([
      '2026-07-01',
      '2026-09-30',
    ]);
  });

  test('read-only structure (Req 5.3): the date-line subtree has no input, button, or anchor', async () => {
    const detail = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    const labels = getDateLineLabels(container);
    expect(labels).toHaveLength(1);
    const dateLineSubtree = labels[0].parentElement;

    // Plain text only — no interactive controls capable of modifying the field.
    expect(
      dateLineSubtree.querySelectorAll('input, button, a, select, textarea')
    ).toHaveLength(0);
  });

  test('role-independence (Req 5.1, 5.2, 5.4): the date line is plain, non-interactive text', async () => {
    // ComplianceDetailPanel does not consume a role/auth context for the
    // estimated-resolution subtree: the line is derived purely from
    // metric.resolution_date and rendered as static spans. The display is
    // therefore role-independent by construction — a viewer, editor, and
    // admin all receive byte-for-byte identical output and no editing control
    // is introduced. We assert the plain-text value and the absence of any
    // interactive element in the subtree.
    const detail = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    expect(getDateLineValueTexts(container)).toEqual(['2026-07-01']);

    const dateLineSubtree = getDateLineLabels(container)[0].parentElement;
    expect(
      dateLineSubtree.querySelectorAll('input, button, a, select, textarea')
    ).toHaveLength(0);
  });

  test('existing editor preserved (Req 4.1): the editable Resolution Date input[type=date] still renders', async () => {
    const detail = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    mockFetch({ details: [detail] });

    const { container } = await renderAndLoad(detail);

    expect(container.querySelector('input[type="date"]')).not.toBeNull();
  });
});

// ===========================================================================
// Task 3.4 — Interaction tests for the existing save round-trip
// ===========================================================================

describe('Task 3.4 — save round-trip', () => {
  test('successful save (Req 4.2, 4.3): displayed estimated-resolution updates to the new date after save + re-fetch', async () => {
    const before = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    const after = makeDetail([
      makeMetric({ resolution_date: '2026-08-15' }),
    ]);
    // GET (initial) -> before, PATCH -> ok, GET (re-fetch) -> after
    mockFetch({ details: [before, after], patchOk: true });

    const { container } = await renderAndLoad(before);

    // Pre-condition: original date displayed.
    expect(getDateLineValueTexts(container)).toEqual(['2026-07-01']);

    // Editor updates the editable Resolution Date field and saves.
    const dateInput = container.querySelector('input[type="date"]');
    fireEvent.change(dateInput, { target: { value: '2026-08-15' } });
    fireEvent.click(screen.getByRole('button', { name: /save/i }));

    // The displayed estimated-resolution value updates from the re-fetch.
    await waitFor(() => {
      expect(getDateLineValueTexts(container)).toEqual(['2026-08-15']);
    });
  });

  test('successful clear (Req 4.5): clearing the field renders NO_DATE_PLACEHOLDER after save + re-fetch', async () => {
    const before = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    const after = makeDetail([makeMetric({ resolution_date: '' })]);
    mockFetch({ details: [before, after], patchOk: true });

    const { container } = await renderAndLoad(before);

    expect(getDateLineValueTexts(container)).toEqual(['2026-07-01']);

    const dateInput = container.querySelector('input[type="date"]');
    fireEvent.change(dateInput, { target: { value: '' } });
    fireEvent.click(screen.getByRole('button', { name: /save/i }));

    await waitFor(() => {
      expect(getDateLineValueTexts(container)).toEqual([NO_DATE_PLACEHOLDER]);
    });
  });

  test('failed save (Req 4.4): previously displayed date is retained and an error is shown', async () => {
    const before = makeDetail([
      makeMetric({ resolution_date: '2026-07-01' }),
    ]);
    // PATCH fails; fetchDetail is never re-issued, so the queue only needs the
    // initial detail.
    mockFetch({
      details: [before],
      patchOk: false,
      patchBody: { error: 'Save failed' },
    });

    const { container } = await renderAndLoad(before);

    expect(getDateLineValueTexts(container)).toEqual(['2026-07-01']);

    const dateInput = container.querySelector('input[type="date"]');
    fireEvent.change(dateInput, { target: { value: '2099-01-01' } });
    fireEvent.click(screen.getByRole('button', { name: /save/i }));

    // Error indication appears.
    await screen.findByText('Save failed');

    // The previously displayed estimated-resolution date is retained.
    expect(getDateLineValueTexts(container)).toEqual(['2026-07-01']);
  });
});
