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

import {
  buildNerdctlAppDeployPlan,
  getTenantAppRuntimeDir,
  getTenantAppSpecPath,
  getTenantAppUnitName,
  getTenantAppUnitPath,
  validateTenantAppSpec,
} from './nerdctl-hosting.js';

describe('nerdctl-hosting helpers', () => {
  it('resolves per-app spec and runtime paths', () => {
    expect(getTenantAppSpecPath('john', 'hello-app')).toBe(
      '/home/john/workspace/apps/hello-app/app.yml',
    );
    expect(getTenantAppRuntimeDir('john', 'hello-app')).toBe(
      '/home/john/.config/nanoclaw/apps/hello-app',
    );
    expect(getTenantAppUnitName('Hello_App')).toBe('nanoclaw-app-hello-app.service');
    expect(getTenantAppUnitPath('john', 'Hello_App')).toBe(
      '/home/john/.config/systemd/user/nanoclaw-app-hello-app.service',
    );
  });

  it('validates a single-service public app spec', () => {
    const result = validateTenantAppSpec({
      tenantId: 'john',
      appName: 'hello',
      appDir: '/home/john/workspace/apps/hello',
      rawSpec: [
        'version: 1',
        'services:',
        '  app:',
        '    build:',
        '      context: .',
        '      dockerfile: Dockerfile',
        '      args:',
        '        CACHEBUST: "1"',
        '    internal_port: 3000',
        '    public: true',
        '    environment:',
        '      PORT: 3000',
        '    command: node server.js',
        '    entrypoint:',
        '      - /bin/sh',
        '      - -lc',
        '    volumes:',
        '      - ./data:/app/data',
      ].join('\n'),
    });

    expect(result.valid).toBe(true);
    expect(result.spec?.publicService?.name).toBe('app');
    expect(result.spec?.services[0].build?.contextPath).toBe(
      '/home/john/workspace/apps/hello',
    );
    expect(result.spec?.services[0].build?.args).toEqual(['CACHEBUST=1']);
    expect(result.spec?.services[0].command).toBe('node server.js');
    expect(result.spec?.services[0].entrypoint).toEqual(['/bin/sh', '-lc']);
    expect(result.spec?.services[0].volumes[0].type).toBe('bind');
    expect(result.spec?.services[0].volumes[0].sourcePath).toBe(
      '/home/john/workspace/apps/hello/data',
    );
    expect(result.spec?.namedVolumes).toEqual([]);
  });

  it('rejects invalid app specs', () => {
    const result = validateTenantAppSpec({
      tenantId: 'john',
      appName: 'hello',
      appDir: '/home/john/workspace/apps/hello',
      rawSpec: [
        'services:',
        '  frontend:',
        '    image: nginx:alpine',
        '    internal_port: 3000',
        '    public: true',
        '  backend:',
        '    build:',
        '      context: .',
        '    internal_port: 3000',
        '    public: true',
        '    depends_on:',
        '      - db',
        '    volumes:',
        '      - /etc:/host-etc',
      ].join('\n'),
    });

    expect(result.valid).toBe(false);
    expect(result.errors).toContain('App spec may define at most one public service.');
    expect(result.errors).toContain(
      'Service "backend" volume "/etc:/host-etc" resolves outside the tenant home.',
    );
    expect(result.errors).toContain(
      'Service "backend" depends on unknown service "db".',
    );
  });

  it('accepts named volumes declared in top-level volumes section', () => {
    const result = validateTenantAppSpec({
      tenantId: 'john',
      appName: 'hello',
      appDir: '/home/john/workspace/apps/hello',
      requirePublicService: false,
      rawSpec: [
        'services:',
        '  db:',
        '    image: mongo:3.4',
        '    volumes:',
        '      - db-data:/data/db',
        'volumes:',
        '  db-data: {}',
      ].join('\n'),
    });

    expect(result.valid).toBe(true);
    expect(result.spec?.namedVolumes).toEqual(['db-data']);
    expect(result.spec?.services[0].volumes[0].type).toBe('named');
    expect(result.spec?.services[0].volumes[0].sourcePath).toBe('db-data');
    expect(result.spec?.services[0].volumes[0].targetPath).toBe('/data/db');
  });

  it('rejects named volumes not declared in top-level volumes section', () => {
    const result = validateTenantAppSpec({
      tenantId: 'john',
      appName: 'hello',
      appDir: '/home/john/workspace/apps/hello',
      rawSpec: [
        'services:',
        '  db:',
        '    image: mongo:3.4',
        '    volumes:',
        '      - db-data:/data/db',
      ].join('\n'),
    });

    expect(result.valid).toBe(false);
    expect(result.errors).toContain(
      'Service "db" references named volume "db-data" not declared in top-level volumes.',
    );
  });

  it('allows multiple services to reuse the same internal port', () => {
    const result = validateTenantAppSpec({
      tenantId: 'john',
      appName: 'hello',
      appDir: '/home/john/workspace/apps/hello',
      rawSpec: [
        'services:',
        '  frontend:',
        '    image: nginx:alpine',
        '    internal_port: 3000',
        '    public: true',
        '  backend:',
        '    image: node:20-alpine',
        '    internal_port: 3000',
      ].join('\n'),
    });

    expect(result.valid).toBe(true);
    expect(result.spec?.publicService?.name).toBe('frontend');
    expect(result.spec?.services).toHaveLength(2);
  });

  it('rejects invalid command and entrypoint blocks', () => {
    const result = validateTenantAppSpec({
      tenantId: 'john',
      appName: 'hello',
      appDir: '/home/john/workspace/apps/hello',
      rawSpec: [
        'services:',
        '  app:',
        '    image: nginx:alpine',
        '    internal_port: 3000',
        '    public: true',
        '    command: []',
        '    entrypoint:',
        '      foo: bar',
      ].join('\n'),
    });

    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Service "app" has an empty command.');
    expect(result.errors).toContain('Service "app" has an invalid entrypoint.');
  });

  it('rejects tenant-defined ports when legacy compatibility is disabled', () => {
    const result = validateTenantAppSpec({
      tenantId: 'john',
      appName: 'hello',
      appDir: '/home/john/workspace/apps/hello',
      allowLegacyPorts: false,
      rawSpec: [
        'services:',
        '  app:',
        '    image: nginx:alpine',
        '    internal_port: 3000',
        '    public: true',
        '    ports:',
        '      - "20801:3000"',
      ].join('\n'),
    });

    expect(result.valid).toBe(false);
    expect(result.errors).toContain(
      'Service "app" cannot define ports. Host port bindings are platform-managed.',
    );
  });

  it('builds a nerdctl deploy plan with compose and systemd units', () => {
    const validation = validateTenantAppSpec({
      tenantId: 'john',
      appName: 'todo',
      appDir: '/home/john/workspace/apps/todo',
      rawSpec: [
        'services:',
        '  web:',
        '    build:',
        '      context: .',
        '      dockerfile: Dockerfile',
        '    internal_port: 3000',
        '    public: true',
        '    depends_on:',
        '      - db',
        '    environment:',
        '      - PORT=3000',
        '    command:',
        '      - node',
        '      - server.js',
        '  db:',
        '    image: docker.io/library/postgres:16',
        '    internal_port: 5432',
      ].join('\n'),
    });

    expect(validation.valid).toBe(true);
    const plan = buildNerdctlAppDeployPlan({
      tenantId: 'john',
      appName: 'todo',
      appDir: '/home/john/workspace/apps/todo',
      hostPort: 20801,
      spec: validation.spec!,
    });

    expect(plan.projectName).toBe('nanoclaw-john-todo');
    expect(plan.unitName).toBe('nanoclaw-app-todo.service');
    expect(plan.publicServiceName).toBe('web');
    expect(plan.publicContainerPort).toBe(3000);
    expect(plan.composeFilePath).toBe(
      '/home/john/.config/nanoclaw/apps/todo/compose.yml',
    );
    expect(plan.composeContent).toContain('127.0.0.1:20801:3000');
    expect(plan.composeContent).toContain('localhost/nanoclaw-john-todo-web:latest');
    expect(plan.composeContent).toContain('docker.io/library/postgres:16');
    expect(plan.composeContent).toContain('name: nanoclaw-john-todo-default');
    expect(plan.composeContent).toContain('command:');
    expect(plan.composeContent).toContain('- node');
    expect(plan.hasBuildServices).toBe(true);
    expect(plan.unitContent).toContain('Requires=nanoclaw-containerd.service');
    expect(plan.unitContent).not.toContain('nanoclaw-buildkitd.service');
    expect(plan.unitContent).toContain('compose --project-name nanoclaw-john-todo -f /home/john/.config/nanoclaw/apps/todo/compose.yml up -d --remove-orphans');
  });
});
