import { Suspense } from 'react';
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { PurchaseDetailClient } from '../purchase-detail-client';

const getMock = vi.fn();
const cancelMock = vi.fn();
const messagesListMock = vi.fn();
const threadsMock = vi.fn();
const returnGetMock = vi.fn();
const myReviewsMock = vi.fn();
vi.mock('@/lib/api', () => ({
  api: {
    purchases: { get: (...a: unknown[]) => getMock(...a) },
    orders: { cancel: (...a: unknown[]) => cancelMock(...a) },
    messages: {
      list: (...a: unknown[]) => messagesListMock(...a),
      post: vi.fn(),
      delete: vi.fn(),
      uploadAttachment: vi.fn(),
    },
    me: { threads: () => threadsMock() },
    returns: {
      get: (...a: unknown[]) => returnGetMock(...a),
      request: vi.fn(),
      cancel: vi.fn(),
      approve: vi.fn(),
      reject: vi.fn(),
      listMine: vi.fn(),
      listSeller: vi.fn(),
    },
    reviews: {
      myReviews: (...a: unknown[]) => myReviewsMock(...a),
      create: vi.fn(),
      update: vi.fn(),
      uploadAttachment: vi.fn(),
    },
  },
}));

function wrap(node: React.ReactNode) {
  const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  return (
    <QueryClientProvider client={qc}>
      <Suspense fallback={<div>loading</div>}>{node}</Suspense>
    </QueryClientProvider>
  );
}

const baseOrder = {
  id: 'ord-1',
  store: { id: 's1', name: 'Revive' },
  status: 'pending',
  is_delayed: false,
  cancellation_reason: null,
  shipped_at: null,
  delivered_at: null,
  tracking_url: null,
  carrier: null,
  tracking_number: null,
  shipping_cost: 500,
  subtotal: 2500,
  items: [
    {
      id: 'oi1',
      title_snapshot: 'Vintage Tee',
      price_snapshot: 2500,
      image_url_snapshot: null,
    },
  ],
};

const basePurchase = {
  id: 'pur-1',
  buyer_id: 'buyer-1',
  subtotal: 2500,
  shipping_total: 500,
  discount_total: 0,
  total: 3000,
  is_delayed: false,
  shipping_address: {
    first_name: 'Jane',
    last_name: 'Doe',
    street: '1 Main St',
    city: 'Portland',
    state: 'OR',
    zip: '97201',
  },
  orders: [baseOrder],
};

function makeParams() {
  return Promise.resolve({ id: 'pur-1' });
}

async function renderAndFlush(node: React.ReactNode) {
  await act(async () => {
    render(node);
    await Promise.resolve();
  });
}

function emptyReviews() {
  return {
    data: [],
    meta: {
      current_page: 1,
      last_page: 1,
      per_page: 20,
      total: 0,
      from: null,
      to: null,
    },
    links: { first: null, last: null, prev: null, next: null },
  };
}

describe('PurchaseDetailClient', () => {
  beforeEach(() => {
    getMock.mockReset();
    cancelMock.mockReset();
    myReviewsMock.mockReset();
    myReviewsMock.mockResolvedValue(emptyReviews());
  });

  it('renders not-found UI when purchase is missing', async () => {
    getMock.mockResolvedValue({ data: null });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await waitFor(() =>
      expect(screen.getByText('Purchase not found')).toBeInTheDocument(),
    );
  });

  it('renders totals, shipping address, and order items', async () => {
    getMock.mockResolvedValue({ data: basePurchase });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await waitFor(() =>
      expect(screen.getByText('Purchase Details')).toBeInTheDocument(),
    );
    expect(screen.getByText('$30.00')).toBeInTheDocument();
    expect(screen.getByText('Jane Doe')).toBeInTheDocument();
    expect(screen.getByText('Vintage Tee')).toBeInTheDocument();
  });

  it('shows delayed banner and "Cancel for full refund" CTA on a delayed pending order', async () => {
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        is_delayed: true,
        orders: [{ ...baseOrder, is_delayed: true }],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await waitFor(() =>
      expect(
        screen.getByRole('button', { name: /Cancel for full refund/ }),
      ).toBeInTheDocument(),
    );
  });

  it('confirm-cancel calls api.orders.cancel with order id', async () => {
    getMock.mockResolvedValue({ data: basePurchase });
    cancelMock.mockResolvedValue({ data: {} });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await waitFor(() =>
      expect(
        screen.getByRole('button', { name: /Cancel order/ }),
      ).toBeInTheDocument(),
    );
    fireEvent.click(screen.getByRole('button', { name: /Cancel order/ }));
    fireEvent.click(screen.getByRole('button', { name: /Confirm cancel/ }));
    await waitFor(() => expect(cancelMock).toHaveBeenCalledWith('ord-1'));
  });

  it('cancelled order shows the cancellation copy for the reason', async () => {
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'cancelled',
            cancellation_reason: 'sold_locally',
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await waitFor(() =>
      expect(
        screen.getByText(/store marked this item as unavailable/i),
      ).toBeInTheDocument(),
    );
  });

  it('renders a messages section per order', async () => {
    getMock.mockResolvedValue({ data: basePurchase });
    messagesListMock.mockResolvedValue({
      data: [],
      meta: { total: 0, has_more: false },
    });
    threadsMock.mockResolvedValue({ data: [] });

    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    const summaries = await screen.findAllByText(/Messages with/i);
    expect(summaries).toHaveLength(basePurchase.orders.length);
  });

  it('shows Request return button when delivered + in window + no open return', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            // Relative dates so the "in window" case stays true regardless of
            // the run date — a hardcoded returns_open_until rots into the past
            // and silently flips this assertion (it did, on 2026-06-16).
            delivered_at: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
            returns_open_until: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
            open_return_id: null,
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    expect(
      await screen.findByRole('button', { name: /Request return/i }),
    ).toBeInTheDocument();
  });

  it('hides Request return button when delivered_at is null', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    getMock.mockResolvedValue({ data: basePurchase });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await waitFor(() =>
      expect(screen.getByText('Vintage Tee')).toBeInTheDocument(),
    );
    expect(
      screen.queryByRole('button', { name: /Request return/i }),
    ).toBeNull();
  });

  it('hides Request return button when window has expired', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            // Relative-past so this "expired window" case stays expired forever.
            delivered_at: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(),
            returns_open_until: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
            open_return_id: null,
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await waitFor(() =>
      expect(screen.getByText('Vintage Tee')).toBeInTheDocument(),
    );
    expect(
      screen.queryByRole('button', { name: /Request return/i }),
    ).toBeNull();
  });

  it('renders the open-return state strip when open_return_id is set', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    returnGetMock.mockResolvedValue({
      data: {
        id: 'r1',
        order_id: 'ord-1',
        initiated_by: 'buyer',
        state: 'requested',
        reason: 'damaged',
        reason_text: null,
        return_shipping_payer: 'seller',
        restocking_fee_cents: 0,
        refund_amount_cents: null,
        stripe_refund_id: null,
        tracking_number: null,
        carrier: null,
        items: [],
        approved_at: null,
        rejected_at: null,
        label_issued_at: null,
        in_transit_at: null,
        received_at: null,
        refunded_at: null,
        closed_at: null,
        cancelled_at: null,
        created_at: '2026-05-07T10:00:00Z',
        updated_at: '2026-05-07T10:00:00Z',
      },
    });
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            delivered_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
            open_return_id: 'r1',
            returns_open_until: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    expect(await screen.findByText('Awaiting seller')).toBeInTheDocument();
    // No request-return button when an open return exists.
    expect(
      screen.queryByRole('button', { name: /Request return/i }),
    ).toBeNull();
  });

  it('shows tracking link in awaiting_shipment / in_transit and Download return label in awaiting_shipment', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    returnGetMock.mockResolvedValue({
      data: {
        id: 'r1',
        order_id: 'ord-1',
        initiated_by: 'buyer',
        state: 'in_transit',
        reason: 'damaged',
        reason_text: null,
        return_shipping_payer: 'seller',
        restocking_fee_cents: 0,
        refund_amount_cents: null,
        stripe_refund_id: null,
        tracking_number: '9400111899223344556677',
        carrier: 'USPS',
        tracker_id: 'trk_test_456',
        tracking_url: 'https://track.easypost.com/trk_test_456',
        shipping_label_url: 'https://easypost-files.s3.amazonaws.com/label.pdf',
        easypost_label_error: null,
        items: [],
        approved_at: '2026-05-07T10:00:00Z',
        rejected_at: null,
        label_issued_at: '2026-05-07T10:30:00Z',
        in_transit_at: '2026-05-07T11:00:00Z',
        received_at: null,
        refunded_at: null,
        closed_at: null,
        cancelled_at: null,
        created_at: '2026-05-07T10:00:00Z',
        updated_at: '2026-05-07T11:00:00Z',
      },
    });
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            delivered_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
            open_return_id: 'r1',
            returns_open_until: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    const trackingLink = await screen.findByRole('link', {
      name: /USPS 9400111899223344556677/i,
    });
    expect(trackingLink.getAttribute('href')).toBe(
      'https://track.easypost.com/trk_test_456',
    );
  });

  it('shows Download return label link in awaiting_shipment', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    const labelUrl = 'https://easypost-files.s3.amazonaws.com/label.pdf';
    returnGetMock.mockResolvedValue({
      data: {
        id: 'r1',
        order_id: 'ord-1',
        initiated_by: 'buyer',
        state: 'awaiting_shipment',
        reason: 'damaged',
        reason_text: null,
        return_shipping_payer: 'seller',
        restocking_fee_cents: 0,
        refund_amount_cents: null,
        stripe_refund_id: null,
        tracking_number: '9400111899223344556677',
        carrier: 'USPS',
        tracker_id: 'trk_test_456',
        tracking_url: 'https://track.easypost.com/trk_test_456',
        shipping_label_url: labelUrl,
        easypost_label_error: null,
        items: [],
        approved_at: '2026-05-07T10:00:00Z',
        rejected_at: null,
        label_issued_at: '2026-05-07T10:30:00Z',
        in_transit_at: null,
        received_at: null,
        refunded_at: null,
        closed_at: null,
        cancelled_at: null,
        created_at: '2026-05-07T10:00:00Z',
        updated_at: '2026-05-07T10:30:00Z',
      },
    });
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            delivered_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
            open_return_id: 'r1',
            returns_open_until: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    const link = await screen.findByRole('link', {
      name: /Download return label/i,
    });
    expect(link.getAttribute('href')).toBe(labelUrl);
    expect(link.getAttribute('target')).toBe('_blank');
  });

  it('hides Download return label link in received state', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    const labelUrl = 'https://easypost-files.s3.amazonaws.com/label.pdf';
    returnGetMock.mockResolvedValue({
      data: {
        id: 'r1',
        order_id: 'ord-1',
        initiated_by: 'buyer',
        state: 'received',
        reason: 'damaged',
        reason_text: null,
        return_shipping_payer: 'seller',
        restocking_fee_cents: 0,
        refund_amount_cents: null,
        stripe_refund_id: null,
        tracking_number: '9400111899223344556677',
        carrier: 'USPS',
        tracker_id: 'trk_test_456',
        tracking_url: 'https://track.easypost.com/trk_test_456',
        shipping_label_url: labelUrl,
        easypost_label_error: null,
        items: [],
        approved_at: '2026-05-07T10:00:00Z',
        rejected_at: null,
        label_issued_at: '2026-05-07T10:30:00Z',
        in_transit_at: '2026-05-07T11:00:00Z',
        received_at: '2026-05-07T13:00:00Z',
        refunded_at: null,
        closed_at: null,
        cancelled_at: null,
        created_at: '2026-05-07T10:00:00Z',
        updated_at: '2026-05-07T13:00:00Z',
      },
    });
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            delivered_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
            open_return_id: 'r1',
            returns_open_until: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await screen.findByTestId('return-state-badge');
    expect(
      screen.queryByRole('link', { name: /Download return label/i }),
    ).toBeNull();
  });

  it('shows unread badge on order with unread messages', async () => {
    getMock.mockResolvedValue({ data: basePurchase });
    messagesListMock.mockResolvedValue({
      data: [],
      meta: { total: 0, has_more: false },
    });
    threadsMock.mockResolvedValue({
      data: [
        {
          thread_id: 't1',
          order_id: baseOrder.id,
          counterparty_name: 'Revive',
          unread_count: 3,
          last_message_snippet: 'hi',
          last_message_at: '2026-05-06T10:00:00Z',
        },
      ],
    });

    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    expect(await screen.findByText(/3 new/i)).toBeInTheDocument();
  });

  it('shows "Leave a review" CTA when order delivered + no existing review', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    myReviewsMock.mockResolvedValue(emptyReviews());
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            delivered_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    expect(
      await screen.findByTestId('review-cta-leave-oi1'),
    ).toBeInTheDocument();
  });

  it('hides review CTA when order is not delivered', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    getMock.mockResolvedValue({ data: basePurchase });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    await waitFor(() =>
      expect(screen.getByText('Vintage Tee')).toBeInTheDocument(),
    );
    expect(screen.queryByTestId('review-cta-leave-oi1')).toBeNull();
  });

  it('shows "Edit your review" CTA when existing review is within 30d', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    myReviewsMock.mockResolvedValue({
      data: [
        {
          id: 'rev-1',
          order_item_id: 'oi1',
          order_id: 'ord-1',
          store_id: 's1',
          reviewer_user_id: 'u1',
          rating: 4,
          rating_item_as_described: 4,
          rating_shipping_speed: 4,
          rating_communication: 4,
          rating_packaging: 4,
          title: null,
          body: 'great',
          state: 'visible',
          edited_at: null,
          photos: [],
          created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
          updated_at: '2026-05-06T10:00:00Z',
        },
      ],
      meta: { current_page: 1, last_page: 1, per_page: 20, total: 1, from: 1, to: 1 },
      links: { first: null, last: null, prev: null, next: null },
    });
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            delivered_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    expect(
      await screen.findByTestId('review-cta-edit-oi1'),
    ).toBeInTheDocument();
  });

  it('shows locked "Reviewed" indicator when existing review is older than 30d', async () => {
    threadsMock.mockResolvedValue({ data: [] });
    myReviewsMock.mockResolvedValue({
      data: [
        {
          id: 'rev-old',
          order_item_id: 'oi1',
          order_id: 'ord-1',
          store_id: 's1',
          reviewer_user_id: 'u1',
          rating: 4,
          rating_item_as_described: 4,
          rating_shipping_speed: 4,
          rating_communication: 4,
          rating_packaging: 4,
          title: null,
          body: 'great',
          state: 'visible',
          edited_at: null,
          photos: [],
          // Relative-past (>30d) so the "older than 30d" lock stays true forever.
          created_at: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(),
          updated_at: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(),
        },
      ],
      meta: { current_page: 1, last_page: 1, per_page: 20, total: 1, from: 1, to: 1 },
      links: { first: null, last: null, prev: null, next: null },
    });
    getMock.mockResolvedValue({
      data: {
        ...basePurchase,
        orders: [
          {
            ...baseOrder,
            status: 'delivered',
            delivered_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
          },
        ],
      },
    });
    await renderAndFlush(wrap(<PurchaseDetailClient params={makeParams()} />));
    expect(
      await screen.findByTestId('review-cta-locked-oi1'),
    ).toBeInTheDocument();
  });
});
