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

const execFileMock = vi.fn()
const fsState = new Map<string, string>()

vi.mock('child_process', () => ({
  execFile: (file: string, args: string[], cb: (error: any, stdout: string, stderr: string) => void) => {
    const result = execFileMock(file, args) as { stdout?: string; stderr?: string } | undefined
    cb(null, result?.stdout ?? '', result?.stderr ?? '')
  },
}))

vi.mock('fs/promises', () => ({
  mkdir: vi.fn().mockResolvedValue(undefined),
  access: vi.fn(async (file: string) => {
    if (!fsState.has(file)) throw Object.assign(new Error('missing'), { code: 'ENOENT' })
  }),
  readFile: vi.fn(async (file: string) => fsState.get(file) ?? '{}'),
  writeFile: vi.fn(async (file: string, contents: string) => { fsState.set(file, contents) }),
}))

vi.mock('../lib/prisma', () => ({
  prisma: {
    companyContainer: {
      findFirst: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      findUniqueOrThrow: vi.fn(),
      findUnique: vi.fn(),
      findMany: vi.fn(),
      delete: vi.fn(),
    },
  },
}))

import * as fs from 'fs/promises'
import { prisma } from '../lib/prisma'
import {
  createCompanyContainer,
  getContainerLogs,
  restartCompanyContainer,
  startCompanyContainer,
  stopCompanyContainer,
  syncContainerStatuses,
} from './containerService'

describe('containerService', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    fsState.clear()
    fsState.set('/opt/companies/docker-compose.companies.yml', 'services: {}')
    fsState.set('/opt/companies/companies-services.json', '{}')
  })

  afterEach(() => {
    vi.resetModules()
  })

  it('allocates the first configured port, writes compose service metadata, starts the service, and stores docker id', async () => {
    vi.mocked(prisma.companyContainer.findFirst).mockResolvedValue(null as never)
    vi.mocked(prisma.companyContainer.create).mockResolvedValue({ id: 'container_row_1' } as never)

    execFileMock.mockImplementation((file: string, args: string[]) => {
      // The child_process mock calls back with empty stdout. This spy records the command shape.
      return { file, args }
    })

    await createCompanyContainer({ id: 'company_1', slug: 'atlas-cars', name: 'Atlas Cars' })

    expect(prisma.companyContainer.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        companyId: 'company_1',
        containerName: 'rdg-company-atlas-cars',
        status: 'CREATING',
        port: 5100,
      }),
    })
    expect(fs.writeFile).toHaveBeenCalledWith(
      '/opt/companies/companies-services.json',
      expect.stringContaining('"atlas-cars"'),
      'utf8',
    )
    expect(fs.writeFile).toHaveBeenCalledWith(
      '/opt/companies/docker-compose.companies.yml',
      expect.stringContaining('container_name: "rdg-company-atlas-cars"'),
      'utf8',
    )
    expect(execFileMock).toHaveBeenCalledWith('docker-compose', ['version'])
    expect(execFileMock).toHaveBeenCalledWith('docker-compose', ['-f', '/opt/companies/docker-compose.companies.yml', 'up', '-d', '--no-recreate', 'company-atlas-cars'])
    expect(prisma.companyContainer.update).toHaveBeenCalledWith({
      where: { id: 'container_row_1' },
      data: { dockerId: null, status: 'RUNNING' },
    })
  })

  it('starts, stops, and restarts an existing company service with status transitions', async () => {
    vi.mocked(prisma.companyContainer.findUniqueOrThrow).mockResolvedValue({
      companyId: 'company_1',
      company: { slug: 'atlas-cars' },
    } as never)

    await startCompanyContainer('company_1')
    await stopCompanyContainer('company_1')
    await restartCompanyContainer('company_1')

    expect(execFileMock).toHaveBeenCalledWith('docker-compose', ['-f', '/opt/companies/docker-compose.companies.yml', 'up', '-d', '--no-recreate', 'company-atlas-cars'])
    expect(execFileMock).toHaveBeenCalledWith('docker-compose', ['-f', '/opt/companies/docker-compose.companies.yml', 'stop', 'company-atlas-cars'])
    expect(execFileMock).toHaveBeenCalledWith('docker-compose', ['-f', '/opt/companies/docker-compose.companies.yml', 'restart', 'company-atlas-cars'])
    expect(prisma.companyContainer.update).toHaveBeenCalledWith({ where: { companyId: 'company_1' }, data: { status: 'RESTARTING', errorMessage: null } })
    expect(prisma.companyContainer.update).toHaveBeenCalledWith({ where: { companyId: 'company_1' }, data: { status: 'RUNNING', errorMessage: null } })
    expect(prisma.companyContainer.update).toHaveBeenCalledWith({ where: { companyId: 'company_1' }, data: { status: 'STOPPED', errorMessage: null } })
  })

  it('returns an empty log string when docker log retrieval fails', async () => {
    vi.mocked(prisma.companyContainer.findUniqueOrThrow).mockResolvedValue({ company: { slug: 'atlas-cars' } } as never)
    execFileMock.mockImplementationOnce(() => { throw new Error('docker unavailable') })

    await expect(getContainerLogs('company_1', 50)).resolves.toBe('')
  })

  it('loads container records and leaves stored statuses unchanged when docker status sync cannot complete', async () => {
    vi.mocked(prisma.companyContainer.findMany).mockResolvedValue([
      { id: 'row_1', status: 'STOPPED', company: { slug: 'atlas-cars' } },
      { id: 'row_2', status: 'RUNNING', company: { slug: 'sahara-rentals' } },
    ] as never)

    execFileMock.mockImplementation(() => { throw new Error('Docker daemon is unavailable') })

    await expect(syncContainerStatuses()).resolves.toBeUndefined()

    expect(prisma.companyContainer.findMany).toHaveBeenCalledWith({
      where: { status: { notIn: ['PENDING', 'CREATING', 'REMOVING'] } },
      include: { company: { select: { slug: true } } },
    })
    expect(prisma.companyContainer.update).not.toHaveBeenCalled()
  })})
