/**
 * Tests for ReferenceResolver
 */

import { ReferenceResolver } from './reference-resolver';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

describe('ReferenceResolver', () => {
  let resolver: ReferenceResolver;
  let tempDir: string;

  beforeEach(() => {
    resolver = new ReferenceResolver();
    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bangstack-ref-test-'));
  });

  afterEach(() => {
    // Clean up temp directory
    if (fs.existsSync(tempDir)) {
      fs.rmSync(tempDir, { recursive: true, force: true });
    }
  });

  describe('basic file references', () => {
    it('should resolve a simple file reference', async () => {
      // Create referenced file
      const referencedFile = path.join(tempDir, 'component.yaml');
      fs.writeFileSync(referencedFile, `
id: login-form
name: LoginForm
description: User login component
`);

      // Create main file with reference
      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        components: [
          { $ref: './component.yaml' }
        ]
      };

      const resolved = await resolver.resolve(obj, { basePath: mainFile });

      expect(resolved.components).toHaveLength(1);
      expect(resolved.components[0].id).toBe('login-form');
      expect(resolved.components[0].name).toBe('LoginForm');
      expect(resolved.components[0].description).toBe('User login component');
    });

    it('should resolve multiple file references', async () => {
      // Create referenced files
      const component1 = path.join(tempDir, 'login.yaml');
      fs.writeFileSync(component1, `
id: login
name: Login
`);

      const component2 = path.join(tempDir, 'signup.yaml');
      fs.writeFileSync(component2, `
id: signup
name: Signup
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        components: [
          { $ref: './login.yaml' },
          { $ref: './signup.yaml' }
        ]
      };

      const resolved = await resolver.resolve(obj, { basePath: mainFile });

      expect(resolved.components).toHaveLength(2);
      expect(resolved.components[0].id).toBe('login');
      expect(resolved.components[1].id).toBe('signup');
    });

    it('should throw error for missing file', async () => {
      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        component: { $ref: './missing.yaml' }
      };

      await expect(resolver.resolve(obj, { basePath: mainFile }))
        .rejects.toThrow('Referenced file not found');
    });
  });

  describe('fragment references', () => {
    it('should resolve a fragment reference', async () => {
      // Create file with nested structure
      const commonFile = path.join(tempDir, 'common.yaml');
      fs.writeFileSync(commonFile, `
auth:
  login:
    method: POST
    path: /api/auth/login
  logout:
    method: POST
    path: /api/auth/logout
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        loginEndpoint: { $ref: './common.yaml#/auth/login' }
      };

      const resolved = await resolver.resolve(obj, { basePath: mainFile });

      expect(resolved.loginEndpoint.method).toBe('POST');
      expect(resolved.loginEndpoint.path).toBe('/api/auth/login');
    });

    it('should resolve array element by index', async () => {
      const itemsFile = path.join(tempDir, 'items.yaml');
      fs.writeFileSync(itemsFile, `
- name: first
  value: 1
- name: second
  value: 2
- name: third
  value: 3
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        selectedItem: { $ref: './items.yaml#/1' }
      };

      const resolved = await resolver.resolve(obj, { basePath: mainFile });

      expect(resolved.selectedItem.name).toBe('second');
      expect(resolved.selectedItem.value).toBe(2);
    });

    it('should throw error for invalid fragment path', async () => {
      const dataFile = path.join(tempDir, 'data.yaml');
      fs.writeFileSync(dataFile, `
existing:
  field: value
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        data: { $ref: './data.yaml#/nonexistent/path' }
      };

      await expect(resolver.resolve(obj, { basePath: mainFile }))
        .rejects.toThrow('Cannot resolve fragment');
    });
  });

  describe('nested references', () => {
    it('should resolve nested references', async () => {
      // Create a chain of references
      const level3 = path.join(tempDir, 'level3.yaml');
      fs.writeFileSync(level3, 'value: deepValue');

      const level2 = path.join(tempDir, 'level2.yaml');
      fs.writeFileSync(level2, `
nested:
  $ref: ./level3.yaml
`);

      const level1 = path.join(tempDir, 'level1.yaml');
      fs.writeFileSync(level1, `
data:
  $ref: ./level2.yaml
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        root: { $ref: './level1.yaml' }
      };

      const resolved = await resolver.resolve(obj, { basePath: mainFile });

      expect(resolved.root.data.nested.value).toBe('deepValue');
    });

    it('should detect circular references', async () => {
      // Create circular reference
      const file1 = path.join(tempDir, 'file1.yaml');
      const file2 = path.join(tempDir, 'file2.yaml');

      fs.writeFileSync(file1, `
ref:
  $ref: ./file2.yaml
`);

      fs.writeFileSync(file2, `
ref:
  $ref: ./file1.yaml
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        data: { $ref: './file1.yaml' }
      };

      await expect(resolver.resolve(obj, { basePath: mainFile }))
        .rejects.toThrow('Circular reference detected');
    });

    it('should enforce maximum depth limit', async () => {
      // Create deep chain
      let currentFile = 'level10.yaml';
      fs.writeFileSync(path.join(tempDir, currentFile), 'value: deep');

      for (let i = 9; i >= 1; i--) {
        const nextFile = `level${i}.yaml`;
        fs.writeFileSync(path.join(tempDir, nextFile), `
nested:
  $ref: ./${currentFile}
`);
        currentFile = nextFile;
      }

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        data: { $ref: './level1.yaml' }
      };

      // Default max depth is 10, should fail with 11+ levels
      await expect(resolver.resolve(obj, { basePath: mainFile, maxDepth: 5 }))
        .rejects.toThrow('Maximum reference depth');
    });
  });

  describe('caching', () => {
    it('should cache resolved files', async () => {
      const sharedFile = path.join(tempDir, 'shared.yaml');
      fs.writeFileSync(sharedFile, `
id: shared-component
name: SharedComponent
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        component1: { $ref: './shared.yaml' },
        component2: { $ref: './shared.yaml' },
        component3: { $ref: './shared.yaml' }
      };

      const cache = new Map();
      const resolved = await resolver.resolve(obj, { basePath: mainFile, cache });

      // Cache should contain the shared file
      expect(cache.size).toBeGreaterThan(0);
      expect(cache.has(sharedFile)).toBe(true);

      // All components should have the same data
      expect(resolved.component1.id).toBe('shared-component');
      expect(resolved.component2.id).toBe('shared-component');
      expect(resolved.component3.id).toBe('shared-component');
    });
  });

  describe('fragment encoding', () => {
    it('should decode JSON Pointer escape sequences', async () => {
      const dataFile = path.join(tempDir, 'data.yaml');
      fs.writeFileSync(dataFile, `
"field~with~tildes":
  value: tilde
"field/with/slashes":
  value: slash
`);

      const mainFile = path.join(tempDir, 'main.yaml');

      // ~0 represents ~, ~1 represents /
      const obj1 = {
        data: { $ref: './data.yaml#/field~0with~0tildes' }
      };
      const resolved1 = await resolver.resolve(obj1, { basePath: mainFile });
      expect(resolved1.data.value).toBe('tilde');

      const obj2 = {
        data: { $ref: './data.yaml#/field~1with~1slashes' }
      };
      const resolved2 = await resolver.resolve(obj2, { basePath: mainFile });
      expect(resolved2.data.value).toBe('slash');
    });
  });

  describe('getAllReferencedFiles', () => {
    it('should collect all referenced files', async () => {
      const comp1 = path.join(tempDir, 'comp1.yaml');
      const comp2 = path.join(tempDir, 'comp2.yaml');
      const shared = path.join(tempDir, 'shared.yaml');

      fs.writeFileSync(comp1, `
id: comp1
common:
  $ref: ./shared.yaml
`);

      fs.writeFileSync(comp2, `
id: comp2
common:
  $ref: ./shared.yaml
`);

      fs.writeFileSync(shared, 'sharedValue: test');

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        components: [
          { $ref: './comp1.yaml' },
          { $ref: './comp2.yaml' }
        ]
      };

      const files = resolver.getAllReferencedFiles(obj, mainFile);

      expect(files).toContain(comp1);
      expect(files).toContain(comp2);
      expect(files).toContain(shared);
      expect(files.length).toBe(3);
    });
  });

  describe('complex scenarios', () => {
    it('should handle mixed references and regular objects', async () => {
      const endpointFile = path.join(tempDir, 'endpoint.yaml');
      fs.writeFileSync(endpointFile, `
method: POST
path: /api/login
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        endpoints: [
          { id: 'login', $ref: './endpoint.yaml' },
          { id: 'logout', method: 'POST', path: '/api/logout' },
          { id: 'refresh', method: 'POST', path: '/api/refresh' }
        ]
      };

      const resolved = await resolver.resolve(obj, { basePath: mainFile });

      expect(resolved.endpoints).toHaveLength(3);
      expect(resolved.endpoints[0].method).toBe('POST');
      expect(resolved.endpoints[0].path).toBe('/api/login');
      expect(resolved.endpoints[1].id).toBe('logout');
      expect(resolved.endpoints[2].id).toBe('refresh');
    });

    it('should handle references within subdirectories', async () => {
      const componentsDir = path.join(tempDir, 'components');
      fs.mkdirSync(componentsDir);

      const formComponent = path.join(componentsDir, 'form.yaml');
      fs.writeFileSync(formComponent, `
type: form
name: LoginForm
`);

      const mainFile = path.join(tempDir, 'main.yaml');
      const obj = {
        ui: {
          component: { $ref: './components/form.yaml' }
        }
      };

      const resolved = await resolver.resolve(obj, { basePath: mainFile });

      expect(resolved.ui.component.type).toBe('form');
      expect(resolved.ui.component.name).toBe('LoginForm');
    });
  });
});
