import type { ConfigService } from '@nestjs/config';
import { LocalStorageService } from './local-storage.service';

function makeService(over: Record<string, string> = {}): LocalStorageService {
  const values: Record<string, string> = {
    'storage.localDir': 'uploads',
    ...over,
  };
  const config = { get: (k: string) => values[k] } as unknown as ConfigService;
  return new LocalStorageService(config);
}

describe('LocalStorageService', () => {
  describe('url / keyFromUrl', () => {
    it('round-trips a key through url() and back', () => {
      const s = makeService();
      const key = 'acme/unused/1712-ab12.pdf';
      const url = s.url(key);
      expect(url).toBe('/api/uploads/acme/unused/1712-ab12.pdf');
      expect(s.keyFromUrl(url)).toBe(key);
    });

    it('accepts an absolute URL whose pathname is the public mount', () => {
      const s = makeService();
      expect(s.keyFromUrl('https://dmc.workzy.co/api/uploads/acme/unused/x.pdf')).toBe(
        'acme/unused/x.pdf',
      );
    });

    it('strips query and hash so they cannot become part of the key', () => {
      const s = makeService();
      expect(s.keyFromUrl('/api/uploads/acme/x.pdf?x=1#frag')).toBe('acme/x.pdf');
    });

    it('returns null for URLs that only contain the mount as a substring', () => {
      const s = makeService();
      expect(s.keyFromUrl('https://evil.com/steal/api/uploads/acme/x.pdf')).toBeNull();
      expect(s.keyFromUrl('https://cdn.example.com/x.png')).toBeNull();
      expect(s.keyFromUrl('')).toBeNull();
    });

    it('does not treat a host-normalised traversal as the original tenant key', () => {
      const s = makeService();
      // URL parser collapses /unused/../.. before we see the path; the
      // resulting key is the OTHER tenant — callers must still bind prefix.
      expect(s.keyFromUrl('https://x.example/api/uploads/acme/unused/../../victim/x.pdf')).toBe(
        'victim/x.pdf',
      );
    });

    it('accepts module media and trashed-media keys only for known module folders', () => {
      const s = makeService();
      expect(s.keyFromUrl('/api/uploads/acme/hotels/1-a.jpg')).toBe('acme/hotels/1-a.jpg');
      expect(s.keyFromUrl('/api/uploads/deleted/acme/hotels/1-a.jpg')).toBe(
        'deleted/acme/hotels/1-a.jpg',
      );
      expect(s.keyFromUrl('/api/uploads/acme/secrets/1-a.jpg')).toBeNull();
      expect(s.keyFromUrl('/api/uploads/other/acme/hotels/1-a.jpg')).toBeNull();
      expect(s.keyFromUrl('/api/uploads/deleted/acme/unused/1-a.pdf')).toBeNull();
    });

    it('returns null for traversal and extra path segments', () => {
      const s = makeService();
      expect(s.keyFromUrl('/api/uploads/acme/unused/../../victim/unused/x.pdf')).toBeNull();
      expect(s.keyFromUrl('/api/uploads/acme/%2e%2e/victim/x.pdf')).toBeNull();
      expect(s.keyFromUrl('/api/uploads/acme/unused/extra/x.pdf')).toBeNull();
    });
  });

  describe('path-traversal containment', () => {
    it('rejects a move whose source key is not a legal storage key', async () => {
      const s = makeService();
      await expect(s.move('../../etc/passwd', 'acme/used/x.pdf')).rejects.toThrow(
        /Unsafe storage key/,
      );
    });

    it('rejects a move whose destination key is not a legal storage key', async () => {
      const s = makeService();
      await expect(s.move('acme/unused/x.pdf', '../../../tmp/evil')).rejects.toThrow(
        /Unsafe storage key/,
      );
    });

    it('rejects in-root cross-tenant traversal keys', async () => {
      const s = makeService();
      await expect(
        s.move('acme/unused/../../victim/unused/x.pdf', 'acme/used/x.pdf'),
      ).rejects.toThrow(/Unsafe storage key/);
    });

    it('rejects delete/exists with a traversal key', async () => {
      const s = makeService();
      await expect(s.delete('../secret')).rejects.toThrow(/Unsafe storage key/);
      await expect(s.exists('acme/../../secret')).rejects.toThrow(/Unsafe storage key/);
    });

    it('rejects STORAGE_LOCAL_DIR that escapes the working directory', () => {
      expect(() => makeService({ 'storage.localDir': '..' })).toThrow(
        /inside the application working directory/,
      );
      expect(() => makeService({ 'storage.localDir': '/etc' })).toThrow(
        /inside the application working directory/,
      );
    });
  });
});
