import fs from 'fs';
import os from 'os';
import path from 'path';

import { afterEach, describe, expect, it, vi } from 'vitest';

import {
  appendRejectionNotice,
  describeRejectedAttachments,
  validateOutboundAttachments,
} from './outbound-attachments.js';

const ONE_PIXEL_PNG = Buffer.from(
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=',
  'base64',
);
const MINIMAL_MP4 = Buffer.from([
  0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00,
  0x00, 0x02, 0x00, 0x69, 0x73, 0x6f, 0x6d, 0x69, 0x73, 0x6f, 0x32,
]);
const MINIMAL_PDF = Buffer.from('%PDF-1.4\n', 'ascii');
const MINIMAL_ZIP = Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00]);

const cleanupDirs: string[] = [];
const cleanupFiles: string[] = [];

function makeTempDir(baseDir: string, prefix: string): string {
  const dir = fs.mkdtempSync(path.join(baseDir, prefix));
  cleanupDirs.push(dir);
  return dir;
}

function writeFile(
  dir: string,
  name: string,
  content: Buffer | string,
): string {
  const filePath = path.join(dir, name);
  fs.writeFileSync(filePath, content);
  return filePath;
}

afterEach(() => {
  vi.unstubAllEnvs();
  vi.restoreAllMocks();
  for (const dir of cleanupDirs.splice(0)) {
    fs.rmSync(dir, { force: true, recursive: true });
  }
  for (const file of cleanupFiles.splice(0)) {
    fs.rmSync(file, { force: true });
  }
});

describe('validateOutboundAttachments', () => {
  it('accepts real image files under default attachment directories', () => {
    const dir = makeTempDir(os.tmpdir(), 'ejclaw-attachment-');
    const imagePath = writeFile(dir, 'screenshot.png', ONE_PIXEL_PNG);

    const result = validateOutboundAttachments([
      {
        path: imagePath,
        name: '../unsafe-name.png',
        mime: 'image/png',
      },
    ]);

    expect(result.rejected).toEqual([]);
    expect(result.files).toEqual([
      {
        attachment: fs.realpathSync(imagePath),
        name: 'unsafe-name.png',
      },
    ]);
  });

  it('accepts direct EJClaw screenshot files under the temp directory', () => {
    const imagePath = path.join(
      os.tmpdir(),
      `ejclaw-room-mobile-list-${Date.now()}.png`,
    );
    fs.writeFileSync(imagePath, ONE_PIXEL_PNG);
    cleanupFiles.push(imagePath);

    const result = validateOutboundAttachments([
      {
        path: imagePath,
        name: 'room-list.png',
        mime: 'image/png',
      },
    ]);

    expect(result.rejected).toEqual([]);
    expect(result.files).toEqual([
      {
        attachment: fs.realpathSync(imagePath),
        name: 'room-list.png',
      },
    ]);
  });

  it('accepts direct generated screenshot files under the temp directory', () => {
    const imagePath = path.join(
      os.tmpdir(),
      `bar-chart-label-fit-playwright-${Date.now()}.png`,
    );
    fs.writeFileSync(imagePath, ONE_PIXEL_PNG);
    cleanupFiles.push(imagePath);

    const result = validateOutboundAttachments([
      {
        path: imagePath,
        name: 'bar-chart-label-fit-playwright.png',
        mime: 'image/png',
      },
    ]);

    expect(result.rejected).toEqual([]);
    expect(result.files).toEqual([
      {
        attachment: fs.realpathSync(imagePath),
        name: 'bar-chart-label-fit-playwright.png',
      },
    ]);
  });

  it('accepts generated image files in nested temp directories', () => {
    const dir = makeTempDir(os.tmpdir(), 'paladin-character-');
    const imagePath = writeFile(dir, 'sheet_12x.png', ONE_PIXEL_PNG);

    const result = validateOutboundAttachments([
      {
        path: imagePath,
        name: 'sheet_12x.png',
        mime: 'image/png',
      },
    ]);

    expect(result.rejected).toEqual([]);
    expect(result.files).toEqual([
      {
        attachment: fs.realpathSync(imagePath),
        name: 'sheet_12x.png',
      },
    ]);
  });

  it('accepts supported non-image media and document files', () => {
    const dir = makeTempDir(os.tmpdir(), 'ejclaw-media-');
    const mp4Path = writeFile(dir, 'preview.mp4', MINIMAL_MP4);
    const pdfPath = writeFile(dir, 'report.pdf', MINIMAL_PDF);
    const zipPath = writeFile(dir, 'archive.zip', MINIMAL_ZIP);
    const textPath = writeFile(dir, 'notes.txt', 'hello\n');

    const result = validateOutboundAttachments([
      { path: mp4Path, mime: 'video/mp4' },
      { path: pdfPath, mime: 'application/pdf' },
      { path: zipPath, mime: 'application/zip' },
      { path: textPath, mime: 'text/plain' },
    ]);

    expect(result.rejected).toEqual([]);
    expect(result.files).toEqual([
      {
        attachment: fs.realpathSync(mp4Path),
        name: 'preview.mp4',
      },
      {
        attachment: fs.realpathSync(pdfPath),
        name: 'report.pdf',
      },
      {
        attachment: fs.realpathSync(zipPath),
        name: 'archive.zip',
      },
      {
        attachment: fs.realpathSync(textPath),
        name: 'notes.txt',
      },
    ]);
  });
});

describe('validateOutboundAttachments policy checks', () => {
  function useIsolatedDefaultTempDir(): void {
    const tempDir = makeTempDir(os.tmpdir(), 'ejclaw-policy-temp-');
    vi.spyOn(os, 'tmpdir').mockReturnValue(tempDir);
  }

  it('requires workspace paths to be explicitly allowlisted', () => {
    useIsolatedDefaultTempDir();
    const dir = makeTempDir(process.cwd(), '.ejclaw-attachment-');
    const imagePath = writeFile(dir, 'workspace-shot.png', ONE_PIXEL_PNG);

    expect(validateOutboundAttachments([{ path: imagePath }])).toEqual({
      files: [],
      rejected: [{ path: imagePath, reason: 'outside-allowed-dirs' }],
    });

    const allowed = validateOutboundAttachments([{ path: imagePath }], {
      baseDirs: [dir],
    });

    expect(allowed.rejected).toEqual([]);
    expect(allowed.files).toEqual([
      {
        attachment: fs.realpathSync(imagePath),
        name: 'workspace-shot.png',
      },
    ]);
  });

  it('accepts user-configured attachment directories from env', () => {
    const unusedCommaDir = makeTempDir(process.cwd(), '.ejclaw-unused-images-');
    const unusedDelimiterDir = makeTempDir(
      process.cwd(),
      '.ejclaw-more-unused-images-',
    );
    const dir = makeTempDir(process.cwd(), '.ejclaw-user-images-');
    const imagePath = writeFile(dir, 'manual-shot.png', ONE_PIXEL_PNG);
    vi.stubEnv(
      'EJCLAW_ATTACHMENT_ALLOWED_DIRS',
      `${unusedCommaDir},${unusedDelimiterDir}${path.delimiter}${dir}`,
    );

    const result = validateOutboundAttachments([{ path: imagePath }]);

    expect(result.rejected).toEqual([]);
    expect(result.files).toEqual([
      {
        attachment: fs.realpathSync(imagePath),
        name: 'manual-shot.png',
      },
    ]);
  });

  it('rejects symlink attempts that escape the allowed directory', () => {
    useIsolatedDefaultTempDir();
    const workspaceDir = makeTempDir(process.cwd(), '.ejclaw-attachment-');
    const targetPath = writeFile(
      workspaceDir,
      'secret-shot.png',
      ONE_PIXEL_PNG,
    );
    const tmpDir = makeTempDir(os.tmpdir(), 'ejclaw-attachment-');
    const linkPath = path.join(tmpDir, 'linked-shot.png');
    fs.symlinkSync(targetPath, linkPath);

    const result = validateOutboundAttachments([{ path: linkPath }]);

    expect(result.files).toEqual([]);
    expect(result.rejected).toEqual([
      { path: linkPath, reason: 'outside-allowed-dirs' },
    ]);
  });

  it('rejects symlink attempts that escape a user-configured directory', () => {
    useIsolatedDefaultTempDir();
    const allowedDir = makeTempDir(process.cwd(), '.ejclaw-user-images-');
    const secretDir = makeTempDir(process.cwd(), '.ejclaw-secret-images-');
    const targetPath = writeFile(secretDir, 'secret-shot.png', ONE_PIXEL_PNG);
    const linkPath = path.join(allowedDir, 'linked-shot.png');
    fs.symlinkSync(targetPath, linkPath);
    vi.stubEnv('EJCLAW_ATTACHMENT_ALLOWED_DIRS', allowedDir);

    const result = validateOutboundAttachments([{ path: linkPath }]);

    expect(result.files).toEqual([]);
    expect(result.rejected).toEqual([
      { path: linkPath, reason: 'outside-allowed-dirs' },
    ]);
  });

  it('rejects SVG attachments before inspecting file content', () => {
    const dir = makeTempDir(os.tmpdir(), 'ejclaw-attachment-');
    const svgPath = writeFile(dir, 'vector.svg', '<svg></svg>');

    const result = validateOutboundAttachments([{ path: svgPath }]);

    expect(result.files).toEqual([]);
    expect(result.rejected).toEqual([
      { path: svgPath, reason: 'unsupported-extension' },
    ]);
  });

  it('rejects files whose extension and image signature do not match policy', () => {
    const dir = makeTempDir(os.tmpdir(), 'ejclaw-attachment-');
    const fakePng = writeFile(dir, 'fake.png', 'not an image');
    const realPng = writeFile(dir, 'real.png', ONE_PIXEL_PNG);

    expect(validateOutboundAttachments([{ path: fakePng }])).toEqual({
      files: [],
      rejected: [{ path: fakePng, reason: 'invalid-image-signature' }],
    });
    expect(
      validateOutboundAttachments([{ path: realPng, mime: 'image/jpeg' }]),
    ).toEqual({
      files: [],
      rejected: [{ path: realPng, reason: 'mime-mismatch' }],
    });
  });

  it('rejects supported non-image files with invalid signatures', () => {
    const dir = makeTempDir(os.tmpdir(), 'ejclaw-media-');
    const fakeMp4 = writeFile(dir, 'fake.mp4', 'not a movie');

    expect(validateOutboundAttachments([{ path: fakeMp4 }])).toEqual({
      files: [],
      rejected: [{ path: fakeMp4, reason: 'invalid-file-signature' }],
    });
  });

  it('rejects non-files and files over the size cap', () => {
    const dir = makeTempDir(os.tmpdir(), 'ejclaw-attachment-');
    const nestedDir = path.join(dir, 'nested.png');
    fs.mkdirSync(nestedDir);
    const largePng = writeFile(
      dir,
      'large.png',
      Buffer.concat([ONE_PIXEL_PNG, Buffer.alloc(8 * 1024 * 1024)]),
    );

    expect(validateOutboundAttachments([{ path: nestedDir }])).toEqual({
      files: [],
      rejected: [{ path: nestedDir, reason: 'not-file' }],
    });
    expect(validateOutboundAttachments([{ path: largePng }])).toEqual({
      files: [],
      rejected: [{ path: largePng, reason: 'too-large' }],
    });
  });
});

describe('describeRejectedAttachments', () => {
  it('returns an empty string when nothing was rejected', () => {
    expect(describeRejectedAttachments([])).toBe('');
  });

  it('summarizes rejected attachments by basename and reason', () => {
    const note = describeRejectedAttachments([
      {
        path: '/home/claude/jarvis-tts/sample.wav',
        reason: 'outside-allowed-dirs',
      },
      { path: '/tmp/missing.png', reason: 'not-found' },
    ]);

    expect(note).toContain('첨부 2건을 전송하지 못했습니다');
    expect(note).toContain('sample.wav (허용된 폴더 밖의 경로)');
    expect(note).toContain('missing.png (파일을 찾을 수 없음)');
  });

  it('falls back to the raw reason code when no label is mapped', () => {
    const note = describeRejectedAttachments([
      { path: '/tmp/x.bin', reason: 'some-new-reason' },
    ]);

    expect(note).toContain('x.bin (some-new-reason)');
  });
});

describe('appendRejectionNotice', () => {
  it('leaves the body untouched when nothing was rejected', () => {
    expect(appendRejectionNotice('보고서입니다', [])).toBe('보고서입니다');
  });

  it('appends the failure notice to the visible body', () => {
    const body = appendRejectionNotice('샘플을 첨부합니다', [
      { path: '/home/claude/jarvis-tts/a.wav', reason: 'outside-allowed-dirs' },
    ]);

    expect(body).toContain('샘플을 첨부합니다');
    expect(body).toContain('첨부 1건을 전송하지 못했습니다');
    expect(body).toContain('a.wav (허용된 폴더 밖의 경로)');
  });

  it('returns the notice alone when the body would otherwise be empty', () => {
    const body = appendRejectionNotice('', [
      { path: '/tmp/missing.png', reason: 'not-found' },
    ]);

    expect(body).toBe(
      describeRejectedAttachments([
        { path: '/tmp/missing.png', reason: 'not-found' },
      ]),
    );
  });
});
