import { describe, it, expect, vi, afterEach } from 'vitest';
import {
  composeCanvasSVG,
  validateForExport,
  triggerDownload,
} from '../exportService';
import type {
  ArtboardConfig,
  CanvasObject,
  RectObject,
  CircleObject,
  EllipseObject,
  LineObject,
  ImageObject,
  TextObject,
} from '../../types/canvas';

// -- Helpers ------------------------------------------------------------------

function makeArtboard(overrides?: Partial<ArtboardConfig>): ArtboardConfig {
  return {
    shape: 'rect',
    width: 4,
    height: 6,
    unit: 'inches',
    ...overrides,
  };
}

function makeRect(overrides?: Partial<RectObject>): RectObject {
  return {
    id: 'rect-1',
    name: 'Rectangle 1',
    type: 'rect',
    x: 10,
    y: 20,
    width: 100,
    height: 50,
    rotation: 0,
    visible: true,
    locked: false,
    opacity: 1,
    fill: '#ff0000',
    stroke: '#000000',
    strokeWidth: 2,
    ...overrides,
  };
}

function makeCircle(overrides?: Partial<CircleObject>): CircleObject {
  return {
    id: 'circle-1',
    name: 'Circle 1',
    type: 'circle',
    x: 50,
    y: 50,
    radius: 25,
    rotation: 0,
    visible: true,
    locked: false,
    opacity: 1,
    fill: '#00ff00',
    stroke: '#000000',
    strokeWidth: 1,
    ...overrides,
  };
}

function makeEllipse(overrides?: Partial<EllipseObject>): EllipseObject {
  return {
    id: 'ellipse-1',
    name: 'Ellipse 1',
    type: 'ellipse',
    x: 100,
    y: 100,
    radiusX: 40,
    radiusY: 20,
    rotation: 0,
    visible: true,
    locked: false,
    opacity: 1,
    fill: '#0000ff',
    stroke: '#000000',
    strokeWidth: 1,
    ...overrides,
  };
}

function makeLine(overrides?: Partial<LineObject>): LineObject {
  return {
    id: 'line-1',
    name: 'Line 1',
    type: 'line',
    x: 0,
    y: 0,
    points: [10, 20, 30, 40, 50, 60],
    rotation: 0,
    visible: true,
    locked: false,
    opacity: 1,
    stroke: '#333333',
    strokeWidth: 3,
    lineStyle: 'solid',
    dash: [],
    ...overrides,
  };
}

function makeImage(overrides?: Partial<ImageObject>): ImageObject {
  return {
    id: 'img-1',
    name: 'Image 1',
    type: 'image',
    x: 10,
    y: 10,
    width: 200,
    height: 150,
    rotation: 0,
    visible: true,
    locked: false,
    opacity: 1,
    src: 'blob:http://localhost:3000/abc-image/svg+xml',
    ...overrides,
  };
}

function makeText(overrides?: Partial<TextObject>): TextObject {
  return {
    id: 'text-1',
    name: 'Text 1',
    type: 'text',
    x: 10,
    y: 10,
    text: 'Hello',
    fontFamily: 'Roboto',
    fontSize: 24,
    letterSpacing: 0,
    lineHeight: 1.2,
    fill: '#000000',
    stroke: '',
    strokeWidth: 0,
    width: 200,
    rotation: 0,
    visible: true,
    locked: false,
    opacity: 1,
    ...overrides,
  };
}

// -- validateForExport --------------------------------------------------------

describe('validateForExport', () => {
  it('returns valid=true with no issues for clean objects', () => {
    const artboard = makeArtboard();
    const objects: CanvasObject[] = [makeRect(), makeCircle()];
    const result = validateForExport(objects, artboard);
    expect(result.valid).toBe(true);
    expect(result.issues).toHaveLength(0);
  });

  it('returns error when artboard is null', () => {
    const result = validateForExport([makeRect()], null);
    expect(result.valid).toBe(false);
    expect(result.issues).toHaveLength(1);
    expect(result.issues[0].severity).toBe('error');
    expect(result.issues[0].message).toMatch(/artboard/i);
  });

  it('returns error for visible text objects', () => {
    const artboard = makeArtboard();
    const text = makeText();
    const result = validateForExport([text], artboard);
    expect(result.valid).toBe(false);
    const textIssue = result.issues.find(
      (i) => i.objectId === 'text-1',
    );
    expect(textIssue).toBeDefined();
    expect(textIssue!.severity).toBe('error');
    expect(textIssue!.message).toMatch(/converted to paths/i);
  });

  it('ignores hidden text objects', () => {
    const artboard = makeArtboard();
    const text = makeText({ visible: false });
    const result = validateForExport([text], artboard);
    expect(result.valid).toBe(true);
    expect(result.issues).toHaveLength(0);
  });

  it('warns on raster images', () => {
    const artboard = makeArtboard();
    const img = makeImage({ src: 'blob:http://localhost:3000/raster-png' });
    const result = validateForExport([img], artboard);
    expect(result.valid).toBe(true);
    const imgIssue = result.issues.find(
      (i) => i.objectId === 'img-1',
    );
    expect(imgIssue).toBeDefined();
    expect(imgIssue!.severity).toBe('warning');
    expect(imgIssue!.message).toMatch(/raster/i);
  });

  it('does not warn on SVG image sources', () => {
    const artboard = makeArtboard();
    const img = makeImage({
      src: 'blob:http://localhost:3000/abc-image/svg+xml',
    });
    const result = validateForExport([img], artboard);
    expect(result.valid).toBe(true);
    expect(result.issues).toHaveLength(0);
  });

  it('collects multiple issues', () => {
    const text = makeText();
    const rasterImg = makeImage({
      id: 'img-raster',
      name: 'Raster',
      src: 'data:image/png;base64,abc',
    });
    const result = validateForExport([text, rasterImg], null);
    expect(result.valid).toBe(false);
    // artboard error + text error + raster warning = 3
    expect(result.issues).toHaveLength(3);
    const errors = result.issues.filter((i) => i.severity === 'error');
    const warnings = result.issues.filter((i) => i.severity === 'warning');
    expect(errors).toHaveLength(2);
    expect(warnings).toHaveLength(1);
  });
});

// -- composeCanvasSVG ---------------------------------------------------------

describe('composeCanvasSVG', () => {
  it('produces SVG with correct viewBox and real-world dimensions (inches)', () => {
    const artboard = makeArtboard({ width: 4, height: 6, unit: 'inches' });
    const svg = composeCanvasSVG([], artboard);
    // 4in * 96 PPI = 384px, 6in * 96 PPI = 576px
    expect(svg).toContain('viewBox="0 0 384 576"');
    expect(svg).toContain('width="4in"');
    expect(svg).toContain('height="6in"');
  });

  it('produces SVG with correct dimensions (mm)', () => {
    const artboard = makeArtboard({ width: 100, height: 200, unit: 'mm' });
    const svg = composeCanvasSVG([], artboard);
    expect(svg).toContain('width="100mm"');
    expect(svg).toContain('height="200mm"');
    // 100mm * (96/25.4) ≈ 377.9528
    expect(svg).toMatch(/viewBox="0 0 377\.95\d* 755\.90\d*"/);
  });

  it('renders rect objects correctly', () => {
    const artboard = makeArtboard();
    const rect = makeRect({ x: 10, y: 20, width: 100, height: 50 });
    const svg = composeCanvasSVG([rect], artboard);
    expect(svg).toContain('<rect');
    expect(svg).toContain('x="10"');
    expect(svg).toContain('y="20"');
    expect(svg).toContain('width="100"');
    expect(svg).toContain('height="50"');
  });

  it('renders circle objects correctly', () => {
    const artboard = makeArtboard();
    const circle = makeCircle({ x: 50, y: 50, radius: 25 });
    const svg = composeCanvasSVG([circle], artboard);
    expect(svg).toContain('<circle');
    expect(svg).toContain('cx="50"');
    expect(svg).toContain('cy="50"');
    expect(svg).toContain('r="25"');
  });

  it('renders ellipse objects correctly', () => {
    const artboard = makeArtboard();
    const ellipse = makeEllipse({ x: 100, y: 100, radiusX: 40, radiusY: 20 });
    const svg = composeCanvasSVG([ellipse], artboard);
    expect(svg).toContain('<ellipse');
    expect(svg).toContain('cx="100"');
    expect(svg).toContain('cy="100"');
    expect(svg).toContain('rx="40"');
    expect(svg).toContain('ry="20"');
  });

  it('renders line objects as polyline', () => {
    const artboard = makeArtboard();
    const line = makeLine({ x: 5, y: 10, points: [0, 0, 20, 30] });
    const svg = composeCanvasSVG([line], artboard);
    expect(svg).toContain('<polyline');
    // Points are offset by obj.x/obj.y: (5+0, 10+0) (5+20, 10+30) → "5,10 25,40"
    expect(svg).toContain('points="5,10 25,40"');
    expect(svg).toContain('fill="none"');
  });

  it('renders SVG images as <image> elements', () => {
    const artboard = makeArtboard();
    const img = makeImage({
      src: 'blob:http://localhost:3000/abc-image/svg+xml',
    });
    const svg = composeCanvasSVG([img], artboard);
    expect(svg).toContain('<image');
    expect(svg).toContain('href=');
  });

  it('skips hidden objects', () => {
    const artboard = makeArtboard();
    const rect = makeRect({ visible: false });
    const svg = composeCanvasSVG([rect], artboard);
    expect(svg).not.toContain('<rect');
  });

  it('skips text objects', () => {
    const artboard = makeArtboard();
    const text = makeText();
    const svg = composeCanvasSVG([text], artboard);
    expect(svg).not.toContain('<text');
    expect(svg).not.toContain('Hello');
  });

  it('skips raster images', () => {
    const artboard = makeArtboard();
    const img = makeImage({
      src: 'data:image/png;base64,abc',
    });
    const svg = composeCanvasSVG([img], artboard);
    expect(svg).not.toContain('<image');
  });

  it('applies rotation transform when non-zero', () => {
    const artboard = makeArtboard();
    const rect = makeRect({ rotation: 45, x: 10, y: 20, width: 100, height: 50 });
    const svg = composeCanvasSVG([rect], artboard);
    // Center: (10+50, 20+25) = (60, 45)
    expect(svg).toContain('transform="rotate(45 60 45)"');
  });

  it('renders multiple objects in order', () => {
    const artboard = makeArtboard();
    const objects: CanvasObject[] = [
      makeRect({ id: 'r1', name: 'R1' }),
      makeCircle({ id: 'c1', name: 'C1' }),
    ];
    const svg = composeCanvasSVG(objects, artboard);
    const rectIdx = svg.indexOf('<rect');
    const circleIdx = svg.indexOf('<circle');
    expect(rectIdx).toBeLessThan(circleIdx);
  });

  it('includes stroke-dasharray for dashed lines', () => {
    const artboard = makeArtboard();
    const line = makeLine({ dash: [5, 3] });
    const svg = composeCanvasSVG([line], artboard);
    expect(svg).toContain('stroke-dasharray="5 3"');
  });
});

// -- triggerDownload ----------------------------------------------------------

describe('triggerDownload', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('creates a blob URL and triggers a download', () => {
    // Activate fake timers before calling triggerDownload so setTimeout is captured
    vi.useFakeTimers();

    const blob = new Blob(['test content'], { type: 'text/plain' });
    const mockUrl = 'blob:http://localhost:3000/mock-blob-url';

    const createObjectURLSpy = vi
      .spyOn(URL, 'createObjectURL')
      .mockReturnValue(mockUrl);
    const revokeObjectURLSpy = vi
      .spyOn(URL, 'revokeObjectURL')
      .mockImplementation(() => {});

    const clickSpy = vi.fn();
    const appendChildSpy = vi.spyOn(document.body, 'appendChild');
    const removeChildSpy = vi.spyOn(document.body, 'removeChild');

    // Mock createElement to return a spy-able anchor
    const mockAnchor = document.createElement('a');
    mockAnchor.click = clickSpy;
    vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor);

    triggerDownload(blob, 'test-file.svg');

    expect(createObjectURLSpy).toHaveBeenCalledWith(blob);
    expect(mockAnchor.href).toBe(mockUrl);
    expect(mockAnchor.download).toBe('test-file.svg');
    expect(clickSpy).toHaveBeenCalledOnce();
    expect(appendChildSpy).toHaveBeenCalledWith(mockAnchor);

    // Cleanup happens after setTimeout — advance timers
    vi.advanceTimersByTime(150);
    expect(removeChildSpy).toHaveBeenCalled();
    expect(revokeObjectURLSpy).toHaveBeenCalledWith(mockUrl);
    vi.useRealTimers();
  });
});
