/**
 * buildEffectiveServiceModel: turns `docker compose config --format json`
 * output into the per-service facts (declared image, build presence,
 * expected replicas, dependencies, healthcheck) that service-scoped update
 * and restore key off of. Fail-closed is the critical property: a render
 * failure must leave `renderable: false` with no services, never a
 * root-file fallback.
 */
import { describe, it, expect, afterEach, vi } from 'vitest';
import { ComposeService } from '../services/ComposeService';
import { buildEffectiveServiceModel } from '../services/effectiveServiceModel';

const ENV_SECRET = 'env-secret-9d4a-value';

function stubRender(rendered: string | null, stderr = '') {
  vi.spyOn(ComposeService, 'getInstance').mockReturnValue({
    renderConfig: vi.fn().mockResolvedValue({ rendered, stderr, code: rendered === null ? 1 : 0, timedOut: false }),
  } as unknown as ComposeService);
}

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

describe('buildEffectiveServiceModel', () => {
  it('extracts declared image, dependsOn, and healthcheck for an image-only service', async () => {
    stubRender(JSON.stringify({
      services: {
        web: {
          image: 'nginx:latest',
          depends_on: { db: { condition: 'service_healthy', required: true } },
          healthcheck: { test: ['CMD', 'true'] },
        },
        db: { image: 'postgres:16' },
      },
    }));
    const result = await buildEffectiveServiceModel(1, 'mystack');
    expect(result.renderable).toBe(true);
    if (!result.renderable) throw new Error('expected renderable');
    expect(result.services).toEqual([
      { name: 'web', declaredImage: 'nginx:latest', hasBuild: false, expectedReplicas: 1, dependsOn: ['db'], hasHealthcheck: true },
      { name: 'db', declaredImage: 'postgres:16', hasBuild: false, expectedReplicas: 1, dependsOn: [], hasHealthcheck: false },
    ]);
  });

  it('flags hasBuild for a build-backed service, including image+build', async () => {
    stubRender(JSON.stringify({
      services: {
        api: { build: { context: '.' } },
        worker: { image: 'app:latest', build: { context: '.' } },
      },
    }));
    const result = await buildEffectiveServiceModel(1, 'mystack');
    if (!result.renderable) throw new Error('expected renderable');
    expect(result.services[0]).toMatchObject({ name: 'api', declaredImage: null, hasBuild: true });
    expect(result.services[1]).toMatchObject({ name: 'worker', declaredImage: 'app:latest', hasBuild: true });
  });

  it('reads expectedReplicas from the top-level scale field, deploy.replicas, or defaults to 1', async () => {
    stubRender(JSON.stringify({
      services: {
        scaled: { image: 'a', scale: 3 },
        deployed: { image: 'b', deploy: { replicas: 2 } },
        zeroed: { image: 'c', scale: 0 },
        unset: { image: 'd' },
      },
    }));
    const result = await buildEffectiveServiceModel(1, 'mystack');
    if (!result.renderable) throw new Error('expected renderable');
    expect(result.services.map(s => s.expectedReplicas)).toEqual([3, 2, 0, 1]);
  });

  it('treats a disabled healthcheck as none', async () => {
    stubRender(JSON.stringify({
      services: { web: { image: 'a', healthcheck: { disable: true } } },
    }));
    const result = await buildEffectiveServiceModel(1, 'mystack');
    if (!result.renderable) throw new Error('expected renderable');
    expect(result.services[0].hasHealthcheck).toBe(false);
  });

  it('treats test NONE as a disabled healthcheck', async () => {
    stubRender(JSON.stringify({
      services: { web: { image: 'a', healthcheck: { test: ['NONE'] } } },
    }));
    const result = await buildEffectiveServiceModel(1, 'mystack');
    if (!result.renderable) throw new Error('expected renderable');
    expect(result.services[0].hasHealthcheck).toBe(false);
  });

  it('parses depends_on given in the short list form', async () => {
    stubRender(JSON.stringify({
      services: { web: { image: 'a', depends_on: ['db', 'cache'] } },
    }));
    const result = await buildEffectiveServiceModel(1, 'mystack');
    if (!result.renderable) throw new Error('expected renderable');
    expect(result.services[0].dependsOn).toEqual(['db', 'cache']);
  });

  it('fails closed with a redacted error and no services when the render errors', async () => {
    stubRender(null, `error: the "${ENV_SECRET}" variable is not set`);
    const result = await buildEffectiveServiceModel(1, 'mystack');
    expect(result.renderable).toBe(false);
    if (result.renderable) throw new Error('expected not renderable');
    expect(result.code).toBe('effective_model_render_failed');
    expect(result.error).not.toContain(ENV_SECRET);
  });

  it('fails closed when the rendered output is not valid JSON', async () => {
    stubRender('this is not json {');
    const result = await buildEffectiveServiceModel(1, 'mystack');
    expect(result.renderable).toBe(false);
    if (result.renderable) throw new Error('expected not renderable');
    expect(result.code).toBe('effective_model_render_failed');
  });

  it('fails closed and redacts the message when renderConfig throws (docker unavailable)', async () => {
    vi.spyOn(ComposeService, 'getInstance').mockReturnValue({
      renderConfig: vi.fn().mockRejectedValue(new Error(`spawn failed: password=${ENV_SECRET}`)),
    } as unknown as ComposeService);
    const result = await buildEffectiveServiceModel(1, 'mystack');
    expect(result.renderable).toBe(false);
    if (result.renderable) throw new Error('expected not renderable');
    expect(result.code).toBe('effective_model_render_failed');
    expect(result.error).not.toContain(ENV_SECRET);
  });

  it('yields an empty service list for a garbage or empty rendered model', async () => {
    stubRender(JSON.stringify({ services: {} }));
    const result = await buildEffectiveServiceModel(1, 'mystack');
    if (!result.renderable) throw new Error('expected renderable');
    expect(result.services).toEqual([]);
  });
});
