import * as fs from 'fs';
import * as path from 'path';
import { tmpdir } from 'os';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ToolContext } from './core.js';
import { executeTool } from './image.js';

function makeWorkspace(): string {
  return fs.mkdtempSync(path.join(tmpdir(), 'maestro-image-'));
}

function makeContext(workspacePath: string, overrides?: Partial<ToolContext>): ToolContext {
  return {
    workspacePath,
    editAllowed: true,
    vlmEnabled: true,
    toolsConfig: {
      visionBaseUrl: 'http://vision.test/v1',
      visionModel: 'vision-model',
    },
    ...overrides,
  };
}

describe('image tools', () => {
  let workspacePath = '';

  beforeEach(() => {
    vi.restoreAllMocks();
  });

  afterEach(() => {
    if (workspacePath) {
      fs.rmSync(workspacePath, { recursive: true, force: true });
      workspacePath = '';
    }
  });

  it('returns image data for LLM context injection', async () => {
    workspacePath = makeWorkspace();
    fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });
    fs.writeFileSync(path.join(workspacePath, 'input', 'capture.jpg'), Buffer.from([0xff, 0xd8, 0xff]));

    const result = await executeTool('ReadImage', {
      file_path: 'input/capture.jpg',
      prompt: 'What is in this image?',
    }, makeContext(workspacePath));

    expect(result?.isError).toBe(false);
    expect(result?.output).toContain('画像を読み込みました: input/capture.jpg');
    expect(result?.output).toContain('What is in this image?');
    expect(result?.images).toHaveLength(1);
    expect(result?.images?.[0]?.dataUrl).toMatch(/^data:image\/jpeg;base64,/);
    expect(result?.images?.[0]?.label).toBe('input/capture.jpg');
  });

  it('rejects ReadImage when vlmEnabled is false', async () => {
    workspacePath = makeWorkspace();
    fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });
    fs.writeFileSync(path.join(workspacePath, 'input', 'capture.jpg'), Buffer.from([0xff, 0xd8, 0xff]));

    const result = await executeTool('ReadImage', {
      file_path: 'input/capture.jpg',
    }, makeContext(workspacePath, { vlmEnabled: false }));

    expect(result?.isError).toBe(true);
    expect(result?.output).toContain('VLM-enabled worker');
  });

  describe('AnnotateImage', () => {
    it('draws a rectangle annotation on an image', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });
      fs.mkdirSync(path.join(workspacePath, 'output'), { recursive: true });

      // Create a real 100x100 red PNG using sharp
      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 100, height: 100, channels: 3, background: { r: 200, g: 200, b: 200 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/annotated.png',
        annotations: [
          { type: 'rectangle', x: 10, y: 10, width: 50, height: 30 },
        ],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(false);
      expect(result!.output).toContain('output/annotated.png');
      expect(result!.output).toContain('1 annotation');
      expect(fs.existsSync(path.join(workspacePath, 'output', 'annotated.png'))).toBe(true);

      // Verify the output is a valid image with same dimensions
      const meta = await sharp(path.join(workspacePath, 'output', 'annotated.png')).metadata();
      expect(meta.width).toBe(100);
      expect(meta.height).toBe(100);
    });

    it('draws multiple annotation types', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });
      fs.mkdirSync(path.join(workspacePath, 'output'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 400, height: 300, channels: 3, background: { r: 255, g: 255, b: 255 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/multi.png',
        annotations: [
          { type: 'rectangle', x: 50, y: 50, width: 100, height: 40, label: 'Button' },
          { type: 'arrow', from_x: 200, from_y: 200, to_x: 100, to_y: 70 },
          { type: 'text', x: 250, y: 150, text: 'Click here' },
        ],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(false);
      expect(result!.output).toContain('3 annotation');
      expect(fs.existsSync(path.join(workspacePath, 'output', 'multi.png'))).toBe(true);
    });

    it('rejects when editAllowed is false', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 100, height: 100, channels: 3, background: { r: 200, g: 200, b: 200 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/annotated.png',
        annotations: [{ type: 'rectangle', x: 10, y: 10, width: 50, height: 30 }],
      }, makeContext(workspacePath, { editAllowed: false }));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(true);
      expect(result!.output).toContain('edit flag is false');
    });

    it('rejects output path outside output/', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 100, height: 100, channels: 3, background: { r: 200, g: 200, b: 200 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'input/evil.png',
        annotations: [{ type: 'rectangle', x: 10, y: 10, width: 50, height: 30 }],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(true);
      expect(result!.output).toContain('must be within');
    });

    it('rejects empty annotations array', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 100, height: 100, channels: 3, background: { r: 200, g: 200, b: 200 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/annotated.png',
        annotations: [],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(true);
      expect(result!.output).toContain('at least one annotation');
    });

    it('uses custom color and font_size', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });
      fs.mkdirSync(path.join(workspacePath, 'output'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 200, height: 200, channels: 3, background: { r: 255, g: 255, b: 255 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/custom.png',
        annotations: [
          { type: 'rectangle', x: 10, y: 10, width: 80, height: 40, color: '#00FF00', label: 'Green box' },
          { type: 'text', x: 10, y: 100, text: 'Custom text', color: '#0000FF', font_size: 20 },
        ],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(false);
      expect(fs.existsSync(path.join(workspacePath, 'output', 'custom.png'))).toBe(true);
    });

    it('handles non-existent input image', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'output'), { recursive: true });

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/missing.png',
        output_path: 'output/annotated.png',
        annotations: [{ type: 'rectangle', x: 10, y: 10, width: 50, height: 30 }],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(true);
    });

    it('rejects invalid color', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 100, height: 100, channels: 3, background: { r: 200, g: 200, b: 200 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/annotated.png',
        annotations: [{ type: 'rectangle', x: 10, y: 10, width: 50, height: 30, color: '"; alert(1); //' }],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(true);
      expect(result!.output).toContain('invalid color');
    });

    it('rejects unknown annotation type', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 100, height: 100, channels: 3, background: { r: 200, g: 200, b: 200 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/annotated.png',
        annotations: [{ type: 'circle' as 'rectangle', x: 10, y: 10, width: 50, height: 30 }],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(true);
      expect(result!.output).toContain('invalid');
      expect(result!.output).toContain('rectangle, arrow, text');
    });

    it('accepts named colors and rgb() colors', async () => {
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });
      fs.mkdirSync(path.join(workspacePath, 'output'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 200, height: 200, channels: 3, background: { r: 255, g: 255, b: 255 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/colors.png',
        annotations: [
          { type: 'rectangle', x: 10, y: 10, width: 50, height: 30, color: 'blue' },
          { type: 'arrow', from_x: 100, from_y: 100, to_x: 150, to_y: 50, color: 'rgb(0, 128, 255)' },
        ],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(false);
    });

    it('renders arrowhead even when color uses rgb() functional notation', async () => {
      // Regression: SVG marker IDs cannot contain spaces/parens/commas, so a color
      // like "rgb(255, 0, 0)" used to produce a malformed url(#ah-rgb(255, 0, 0))
      // and the arrowhead would silently fail to render.
      // Use a 2000x2000 image to get strokeWidth=5 and markerHeight=10.5 from
      // auto-scaling — that gives enough room to sample arrowhead-only pixels
      // (perpendicular to and clearly outside the line stroke itself).
      workspacePath = makeWorkspace();
      fs.mkdirSync(path.join(workspacePath, 'input'), { recursive: true });
      fs.mkdirSync(path.join(workspacePath, 'output'), { recursive: true });

      const sharp = (await import('sharp')).default;
      const pngBuffer = await sharp({
        create: { width: 2000, height: 2000, channels: 3, background: { r: 255, g: 255, b: 255 } },
      }).png().toBuffer();
      fs.writeFileSync(path.join(workspacePath, 'input', 'screen.png'), pngBuffer);

      const result = await executeTool('AnnotateImage', {
        input_path: 'input/screen.png',
        output_path: 'output/arrow_rgb.png',
        annotations: [
          { type: 'arrow', from_x: 500, from_y: 1000, to_x: 1500, to_y: 1000, color: 'rgb(255, 0, 0)' },
        ],
      }, makeContext(workspacePath));

      expect(result).not.toBeNull();
      expect(result!.isError).toBe(false);

      // With stroke=5, the line covers y=997..1002. The arrowhead extends
      // y=995..1005. Sampling at y=994..995 lands in the arrowhead-only band:
      // any red pixel here proves the marker URL resolved correctly.
      const { data, info } = await sharp(path.join(workspacePath, 'output', 'arrow_rgb.png'))
        .raw()
        .toBuffer({ resolveWithObject: true });

      let arrowheadReds = 0;
      for (let y = 993; y <= 995; y++) {
        for (let x = 1485; x <= 1500; x++) {
          const idx = (y * info.width + x) * info.channels;
          const r = data[idx]!;
          const g = data[idx + 1]!;
          const b = data[idx + 2]!;
          if (r > 150 && g < 100 && b < 100) arrowheadReds++;
        }
      }
      expect(arrowheadReds).toBeGreaterThan(0);
    });
  });
});
