/** @vitest-environment jsdom */
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
  AISTHETIX_WORDMARK,
  AISTHETIX_WATERMARK_OPACITY,
  FEED_HEIGHT,
  FEED_WIDTH,
  CREATED_WITH_COPY,
  STORY_COPY,
  STORY_HEIGHT,
  STORY_WIDTH,
  buildShareMessage,
  composeShareImage,
  containRect,
  copyProductLink,
  detectShareCapability,
  detectSharePayloadMode,
  downloadShareImage,
  drawShareComposition,
  openShareSheet,
  readableMerchantDomain,
} from './share';

const metadata = {
  productName: 'Blue Linen Dress',
  productUrl: 'https://merchant.example/products/blue-dress?variant=42',
  merchantName: 'Merchant Atelier',
  merchantDomain: 'https://merchant.example',
};

function createCanvasContext() {
  return {
    fillStyle: '', strokeStyle: '', font: '', textAlign: 'start', textBaseline: 'alphabetic',
    globalAlpha: 1, shadowColor: '', shadowBlur: 0,
    lineWidth: 0, lineJoin: 'miter',
    fillRect: vi.fn(), drawImage: vi.fn(), fillText: vi.fn(),
    save: vi.fn(), restore: vi.fn(), beginPath: vi.fn(), moveTo: vi.fn(),
    lineTo: vi.fn(), closePath: vi.fn(), stroke: vi.fn(),
  } as unknown as CanvasRenderingContext2D;
}

afterEach(() => {
  vi.restoreAllMocks();
  vi.unstubAllGlobals();
  vi.useRealTimers();
});

describe('channel-aware compositor', () => {
  it('provides Story 9:16 and feed-safe 4:5 dimensions', () => {
    expect([STORY_WIDTH, STORY_HEIGHT]).toEqual([1080, 1920]);
    expect([FEED_WIDTH, FEED_HEIGHT]).toEqual([1080, 1350]);
    expect(containRect(2000, 1000, { x: 0, y: 0, width: 1000, height: 1000 })).toEqual({ x: 0, y: 250, width: 1000, height: 500 });
    expect(() => containRect(0, 100, { x: 0, y: 0, width: 10, height: 10 })).toThrow('invalid image dimensions');
  });

  it.each(['story', 'feed'] as const)('bakes immutable branding and verified product/store references into %s', (format) => {
    const context = createCanvasContext();
    const fillText = vi.mocked(context.fillText);
    const source = {} as CanvasImageSource;
    const rect = drawShareComposition(context, source, 1600, 900, metadata, format);
    expect(context.drawImage).toHaveBeenCalledWith(source, rect.x, rect.y, rect.width, rect.height);
    expect(rect.width / rect.height).toBeCloseTo(1600 / 900);
    expect(fillText).not.toHaveBeenCalledWith('AISTHETIX', expect.anything(), expect.anything(), expect.anything());
    expect(fillText).not.toHaveBeenCalledWith(STORY_COPY, expect.anything(), expect.anything(), expect.anything());
    const watermarkRight = rect.x + rect.width - 24;
    const watermarkY = rect.y + rect.height - 38;
    expect(fillText).not.toHaveBeenCalledWith('A', expect.anything(), expect.anything(), expect.anything());
    expect(fillText).toHaveBeenCalledWith(CREATED_WITH_COPY, watermarkRight - 160, watermarkY, 80);
    expect(fillText).toHaveBeenCalledWith(AISTHETIX_WORDMARK, watermarkRight, watermarkY, 190);
    expect(context.globalAlpha).toBe(AISTHETIX_WATERMARK_OPACITY);
    expect(context.stroke).not.toHaveBeenCalled();
    expect(fillText).not.toHaveBeenCalledWith('Blue Linen Dress', expect.any(Number), expect.any(Number), expect.any(Number));
    expect(fillText).toHaveBeenCalledWith('Merchant Atelier', expect.any(Number), expect.any(Number), 900);
    expect(fillText).toHaveBeenCalledWith('merchant.example', expect.any(Number), expect.any(Number), 900);
  });

  it('exports separate branded canvases without mutating the raw source', async () => {
    const source = 'data:image/png;base64,RAW_RESULT_MUST_NOT_BE_SHARED';
    class LoadedImage {
      decoding = ''; naturalWidth = 1600; naturalHeight = 900; width = 1600; height = 900;
      onload: (() => void) | null = null; onerror: (() => void) | null = null;
      set src(_value: string) { this.onload?.(); }
    }
    vi.stubGlobal('Image', LoadedImage);
    const canvas = {
      width: 0, height: 0,
      getContext: () => createCanvasContext(),
      toBlob: (callback: BlobCallback) => callback(new Blob(['COMPOSED_BRANDED'], { type: 'image/png' })),
    };
    vi.spyOn(document, 'createElement').mockReturnValue(canvas as unknown as HTMLCanvasElement);
    await expect(composeShareImage(source, metadata, 'story')).resolves.toHaveProperty('type', 'image/png');
    expect([canvas.width, canvas.height]).toEqual([1080, 1920]);
    await composeShareImage(source, metadata, 'feed');
    expect([canvas.width, canvas.height]).toEqual([1080, 1350]);
    expect(source).toContain('RAW_RESULT_MUST_NOT_BE_SHARED');
  });

  it('never falls back to the raw source when decoding fails', async () => {
    class BrokenImage {
      decoding = ''; onload: (() => void) | null = null; onerror: (() => void) | null = null;
      set src(_value: string) { this.onerror?.(); }
    }
    vi.stubGlobal('Image', BrokenImage);
    await expect(composeShareImage('data:image/png;base64,RAW', metadata, 'story')).rejects.toThrow('could not be decoded');
  });

  it('normalizes a verified merchant URL to a readable domain', () => {
    expect(readableMerchantDomain(metadata)).toBe('merchant.example');
    expect(readableMerchantDomain({ productName: '', productUrl: null })).toBe('');
    expect(readableMerchantDomain({ productName: '', productUrl: null, merchantDomain: 'not a url/path' })).toBe('not a url');
    expect(readableMerchantDomain({ productName: '', productUrl: null, merchantDomain: 'www.shop.example' })).toBe('shop.example');
  });

  it('fails closed when a canvas or PNG export is unavailable', async () => {
    class LoadedImage {
      decoding = ''; naturalWidth = 10; naturalHeight = 10; width = 10; height = 10;
      onload: (() => void) | null = null; onerror: (() => void) | null = null;
      set src(_value: string) { this.onload?.(); }
    }
    vi.stubGlobal('Image', LoadedImage);
    vi.spyOn(document, 'createElement').mockReturnValue({ getContext: () => null } as unknown as HTMLCanvasElement);
    await expect(composeShareImage('data:image/png;base64,RAW', metadata, 'story')).rejects.toThrow('canvas unavailable');
    vi.restoreAllMocks();
    vi.spyOn(document, 'createElement').mockReturnValue({
      width: 0, height: 0,
      getContext: () => createCanvasContext(),
      toBlob: (callback: BlobCallback) => callback(null),
    } as unknown as HTMLCanvasElement);
    await expect(composeShareImage('data:image/png;base64,RAW', metadata, 'story')).rejects.toThrow('export failed');
  });

  it('downloads the composed PNG through a connected link and keeps the blob valid for deferred browser handling', () => {
    vi.useFakeTimers();
    const createObjectURL = vi.fn(() => 'blob:composed');
    const revokeObjectURL = vi.fn();
    vi.stubGlobal('URL', { createObjectURL, revokeObjectURL });
    let connectedAtClick = false;
    let anchor: HTMLAnchorElement | undefined;
    vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) {
      anchor = this;
      connectedAtClick = this.isConnected;
    });
    const composed = new Blob(['COMPOSED_BRANDED'], { type: 'image/png' });
    downloadShareImage(composed, 'feed');
    expect(createObjectURL).toHaveBeenCalledWith(composed);
    expect(connectedAtClick).toBe(true);
    expect(anchor?.download).toBe('aisthetix-try-on-feed.png');
    expect(anchor?.isConnected).toBe(false);
    vi.advanceTimersByTime(29_999);
    expect(revokeObjectURL).not.toHaveBeenCalled();
    vi.advanceTimersByTime(1);
    expect(revokeObjectURL).toHaveBeenCalledWith('blob:composed');
  });

});

describe('guided channel handoff', () => {
  const file = new File(['COMPOSED_BRANDED'], 'feed.png', { type: 'image/png' });

  it('builds a WhatsApp caption with product and exact HTTPS variant URL', () => {
    expect(buildShareMessage(metadata)).toBe(`Blue Linen Dress\n${metadata.productUrl}\n${STORY_COPY}`);
    expect(buildShareMessage({ productName: 'Dress', productUrl: null })).toBe(`Dress\n${STORY_COPY}`);
  });

  it('uses file+text only when the browser confirms the whole payload', async () => {
    const share = vi.fn().mockResolvedValue(undefined);
    const nav = { share, canShare: vi.fn((data: ShareData) => Boolean(data.files)) } as unknown as Navigator;
    expect(detectSharePayloadMode(file, metadata, nav)).toBe('file_text');
    expect(detectShareCapability(file, nav, metadata)).toBe('file_share');
    await expect(openShareSheet(file, metadata, nav)).resolves.toEqual({ capability: 'file_share', outcome: 'share_sheet_closed', payloadMode: 'file_text' });
    expect(share).toHaveBeenCalledWith(expect.objectContaining({ files: [file] }));
    expect(share.mock.calls[0][0].text).toContain(metadata.productUrl);
    expect(share.mock.calls[0][0].url).toBeUndefined();
  });

  it('detects Safari-style file-only sharing so the UI can require paste', async () => {
    const share = vi.fn().mockResolvedValue(undefined);
    const nav = { share, canShare: vi.fn((data: ShareData) => Boolean(data.files && !data.text)) } as unknown as Navigator;
    expect(detectSharePayloadMode(file, metadata, nav)).toBe('file_only');
    await openShareSheet(file, metadata, nav);
    expect(share).toHaveBeenCalledWith({ files: [file] });
  });

  it('downloads when files are unsupported and reports cancellation or failure honestly', async () => {
    const linkNav = { share: vi.fn().mockResolvedValue(undefined), canShare: vi.fn(() => false) } as unknown as Navigator;
    expect(detectShareCapability(file, linkNav, metadata)).toBe('download_only');
    await expect(openShareSheet(file, metadata, linkNav)).resolves.toEqual({ capability: 'download_only', outcome: 'fallback_ready', payloadMode: 'none' });
    const cancelled = await openShareSheet(file, metadata, { share: vi.fn().mockRejectedValue(new DOMException('cancelled', 'AbortError')), canShare: vi.fn(() => true) } as unknown as Navigator);
    expect(cancelled.outcome).toBe('cancelled');
    const failed = await openShareSheet(file, metadata, { share: vi.fn().mockRejectedValue(new Error('unavailable')), canShare: vi.fn(() => true) } as unknown as Navigator);
    expect(failed.outcome).toBe('failed');
  });

  it('returns explicit clipboard/download fallbacks when native sharing is absent or broken', async () => {
    const clipboard = { writeText: vi.fn() };
    expect(detectShareCapability(file, { clipboard } as unknown as Navigator, metadata)).toBe('download_clipboard');
    expect(detectShareCapability(file, {} as Navigator, metadata)).toBe('download_only');
    const throwing = { share: vi.fn(), canShare: vi.fn(() => { throw new Error('partial implementation'); }) } as unknown as Navigator;
    expect(detectSharePayloadMode(file, metadata, throwing)).toBe('none');
    await expect(openShareSheet(file, { ...metadata, productUrl: null }, {} as Navigator)).resolves.toEqual({ capability: 'download_only', outcome: 'fallback_ready', payloadMode: 'none' });
  });

  it('reports clipboard denial without logging or throwing', async () => {
    await expect(copyProductLink(metadata.productUrl, { clipboard: { writeText: vi.fn().mockRejectedValue(new Error('denied')) } } as unknown as Navigator)).resolves.toBe(false);
  });
});


it('bounds clipboard copying when a social popup moves focus away', async () => {
  vi.useFakeTimers();
  const nav = { clipboard: { writeText: vi.fn(() => new Promise<void>(() => {})) } } as unknown as Navigator;
  const copied = copyProductLink('https://store.example/dress', nav);
  await vi.advanceTimersByTimeAsync(2000);
  await expect(copied).resolves.toBe(false);
  expect(vi.getTimerCount()).toBe(0);
});


it('never opens a text/link share sheet when the browser cannot share the photo', async () => {
  const share = vi.fn();
  const nav = { share, canShare: vi.fn((data: ShareData) => !data.files || Boolean(data.text)) } as unknown as Navigator;
  const file = new File(['PHOTO'], 'story.png', { type: 'image/png' });
  await expect(openShareSheet(file, metadata, nav)).resolves.toMatchObject({ outcome: 'fallback_ready', payloadMode: 'none' });
  expect(share).not.toHaveBeenCalled();
});


it.each([
  ['desktop Chrome', { userAgentData: { mobile: false }, userAgent: 'Mozilla Macintosh', maxTouchPoints: 0 }, false],
  ['Android Chrome', { userAgentData: { mobile: true }, userAgent: 'Mozilla Android' }, true],
  ['iPhone Safari', { userAgent: 'Mozilla iPhone', maxTouchPoints: 5 }, true],
  ['iPad desktop UA', { userAgent: 'Mozilla Macintosh', maxTouchPoints: 5 }, true],
  ['touch desktop with hint', { userAgentData: { mobile: false }, userAgent: 'Mozilla Macintosh', maxTouchPoints: 5 }, false],
])('identifies %s independently of file sharing capability', async (_label, nav, expected) => {
  const { isMobileShareDevice } = await import('./share');
  expect(isMobileShareDevice(nav as unknown as Navigator)).toBe(expected);
});
