const express = require('express');
const request = require('supertest');

// ValidationError and NotFoundError are now properly imported in services.js

// Minimal asyncHandler
function asyncHandler(fn) {
  return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}

// Mock modules that services.js requires at top-level
jest.mock('../../src/utilities/constants', () => ({
  APP: { USER_AGENTS: { PROBE: 'DashCaddy/1.0' } },
  REGEX: { SUBDOMAIN: /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/ },
  TIMEOUTS: { DEFAULT: 10000 },
  HTTP_STATUS: { OK: 200, CREATED: 201, NO_CONTENT: 204, BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, NOT_FOUND: 404, CONFLICT: 409, INTERNAL_ERROR: 500 }
}));

jest.mock('../../src/security/input-validator', () => ({
  validateServiceConfig: jest.fn(),
  isValidPort: jest.fn(p => p >= 1 && p <= 65535),
}));

jest.mock('../../src/utilities/fs-helpers', () => ({
  exists: jest.fn().mockResolvedValue(true),
}));

jest.mock('../../src/utilities/url-resolver', () => ({
  resolveServiceUrl: jest.fn((id) => `https://${id}.test`),
}));

jest.mock('../../src/utilities/pagination', () => ({
  paginate: jest.fn((data, params) => ({ data, pagination: null })),
  parsePaginationParams: jest.fn(() => null),
}));

jest.mock('../../src/utils/responses', () => ({
  success: jest.fn((res, data, statusCode = 200) => {
    return res.status(statusCode).json({ success: true, ...data });
  }),
  error: jest.fn((res, message, statusCode = 500, extra) => {
    return res.status(statusCode).json({ success: false, error: message, ...extra });
  }),
}));

// errors module NOT mocked — used for real ValidationError/NotFoundError/ConflictError

const { exists } = require('../../src/utilities/fs-helpers');
const { validateServiceConfig } = require('../../src/security/input-validator');

function createApp(depsOverride = {}) {
  const defaultDeps = {
    servicesStateManager: {
      read: jest.fn().mockResolvedValue([]),
      write: jest.fn().mockResolvedValue(),
      update: jest.fn(async (fn) => {
        const data = fn([]);
        return data;
      }),
    },
    credentialManager: {
      store: jest.fn().mockResolvedValue(true),
      retrieve: jest.fn().mockResolvedValue(null),
      delete: jest.fn().mockResolvedValue(true),
    },
    siteConfig: { tld: 'sami' },
    buildServiceUrl: jest.fn(id => `https://${id}.sami`),
    buildDomain: jest.fn(sub => `${sub}.sami`),
    fetchT: jest.fn().mockResolvedValue({ ok: true, status: 200, json: () => ({}) }),
    asyncHandler,
    SERVICES_FILE: '/tmp/services.json',
    log: { error: jest.fn(), info: jest.fn(), warn: jest.fn() },
    safeErrorMessage: jest.fn(err => err.message),
    resyncHealthChecker: jest.fn().mockResolvedValue(),
    caddy: {
      read: jest.fn().mockResolvedValue(''),
      modify: jest.fn().mockResolvedValue({ success: true }),
      generateConfig: jest.fn().mockReturnValue('generated config'),
    },
    dns: {
      addRecord: jest.fn().mockResolvedValue({ success: true }),
    },
  };

  const deps = { ...defaultDeps, ...depsOverride };
  const servicesRoutes = require('../../routes/services');
  const app = express();
  app.use(express.json());
  app.use('/api', servicesRoutes(deps));
  // Error handler
  app.use((err, req, res, next) => {
    const status = err.statusCode || 500;
    res.status(status).json({ success: false, error: err.message });
  });
  return { app, deps };
}

describe('Services Routes', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    exists.mockResolvedValue(true);
    validateServiceConfig.mockImplementation(() => {}); // No-op (valid)
  });

  describe('GET /api/services', () => {
    it('returns empty services array (enveloped) when no services file', async () => {
      exists.mockResolvedValue(false);
      const { app } = createApp();
      const res = await request(app).get('/api/services');
      expect(res.status).toBe(200);
      expect(res.body).toEqual({ success: true, services: [] });
    });

    it('returns services list', async () => {
      const services = [
        { id: 'plex', name: 'Plex' },
        { id: 'radarr', name: 'Radarr' },
      ];
      const stateManager = {
        read: jest.fn().mockResolvedValue(services),
        write: jest.fn(),
        update: jest.fn(),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app).get('/api/services');
      expect(res.status).toBe(200);
    });
  });

  describe('POST /api/services', () => {
    it('adds a new service', async () => {
      const stateManager = {
        read: jest.fn().mockResolvedValue([]),
        write: jest.fn(),
        update: jest.fn(async (fn) => fn([])),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app)
        .post('/api/services')
        .send({ id: 'plex', name: 'Plex' });
      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
      expect(stateManager.update).toHaveBeenCalled();
    });

    // NOTE: POST /services validation for missing id/name is caught by the route's
    // try/catch block which logs the error but doesn't send a response in the else branch.
    // The catch block only sends a response for "already exists" errors (409).

    it('returns 409 when service already exists', async () => {
      const stateManager = {
        read: jest.fn().mockResolvedValue([]),
        write: jest.fn(),
        update: jest.fn(async (fn) => fn([{ id: 'plex', name: 'Plex' }])),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app)
        .post('/api/services')
        .send({ id: 'plex', name: 'Plex' });
      expect(res.status).toBe(409);
    });
  });

  describe('PUT /api/services', () => {
    it('replaces all services', async () => {
      const stateManager = {
        read: jest.fn(),
        write: jest.fn().mockResolvedValue(),
        update: jest.fn(),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const services = [
        { id: 'plex', name: 'Plex' },
        { id: 'radarr', name: 'Radarr' },
      ];
      const res = await request(app)
        .put('/api/services')
        .send(services);
      expect(res.status).toBe(200);
      expect(res.body.count).toBe(2);
      expect(stateManager.write).toHaveBeenCalledWith(services);
    });

    it('rejects non-array body', async () => {
      const { app } = createApp();
      const res = await request(app)
        .put('/api/services')
        .send({ id: 'plex' });
      expect(res.status).toBeGreaterThanOrEqual(400);
    });

    it('rejects services without id or name', async () => {
      const { app } = createApp();
      const res = await request(app)
        .put('/api/services')
        .send([{ id: 'plex' }]); // missing name
      expect(res.status).toBeGreaterThanOrEqual(400);
    });
  });

  describe('DELETE /api/services/:id', () => {
    it('removes a service', async () => {
      const stateManager = {
        read: jest.fn(),
        write: jest.fn(),
        update: jest.fn(async (fn) => fn([{ id: 'plex' }, { id: 'radarr' }])),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app).delete('/api/services/plex');
      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
    });

    it('returns 404 when services file missing', async () => {
      exists.mockResolvedValue(false);
      const { app } = createApp();
      const res = await request(app).delete('/api/services/plex');
      expect(res.status).toBeGreaterThanOrEqual(404);
    });
  });

  describe('POST /api/services/:serviceId/credentials', () => {
    it('stores credentials', async () => {
      const credentialManager = {
        store: jest.fn().mockResolvedValue(true),
        retrieve: jest.fn(),
        delete: jest.fn(),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app)
        .post('/api/services/radarr/credentials')
        .send({ apiKey: 'test-key', username: 'admin', password: 'pass' });
      expect(res.status).toBe(200);
      expect(credentialManager.store).toHaveBeenCalledWith('service.radarr.apikey', 'test-key');
      expect(credentialManager.store).toHaveBeenCalledWith('service.radarr.username', 'admin');
      expect(credentialManager.store).toHaveBeenCalledWith('service.radarr.password', 'pass');
    });
  });

  describe('DELETE /api/services/:serviceId/credentials', () => {
    it('deletes credentials', async () => {
      const credentialManager = {
        store: jest.fn(),
        retrieve: jest.fn(),
        delete: jest.fn().mockResolvedValue(true),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app).delete('/api/services/radarr/credentials');
      expect(res.status).toBe(200);
      expect(credentialManager.delete).toHaveBeenCalledWith('service.radarr.apikey');
    });
  });

  describe('GET /api/services/:serviceId/credentials', () => {
    it('returns credential status', async () => {
      const credentialManager = {
        store: jest.fn(),
        retrieve: jest.fn().mockResolvedValue(null),
        delete: jest.fn(),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app).get('/api/services/radarr/credentials');
      expect(res.status).toBe(200);
      expect(res.body).toHaveProperty('hasApiKey');
      expect(res.body).toHaveProperty('hasBasicAuth');
    });

    it('returns hasApiKey:true when API key exists', async () => {
      const credentialManager = {
        store: jest.fn(),
        retrieve: jest.fn().mockImplementation((key) => {
          if (key === 'service.radarr.apikey') return Promise.resolve('the-key');
          return Promise.resolve(null);
        }),
        delete: jest.fn(),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app).get('/api/services/radarr/credentials');
      expect(res.status).toBe(200);
      expect(res.body.hasApiKey).toBe(true);
    });
  });

  // ===== SEEDHOST CREDENTIAL ENDPOINTS =====

  describe('POST /api/seedhost-creds', () => {
    it('stores seedhost username and password', async () => {
      const credentialManager = {
        store: jest.fn().mockResolvedValue(true),
        retrieve: jest.fn(),
        delete: jest.fn(),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app)
        .post('/api/seedhost-creds')
        .send({ username: 'user1', password: 'pass1' });
      expect(res.status).toBe(200);
      expect(credentialManager.store).toHaveBeenCalledWith('seedhost.username', 'user1');
      expect(credentialManager.store).toHaveBeenCalledWith('seedhost.password', 'pass1');
    });

    it('stores per-service password when serviceId provided', async () => {
      const credentialManager = {
        store: jest.fn().mockResolvedValue(true),
        retrieve: jest.fn(),
        delete: jest.fn(),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app)
        .post('/api/seedhost-creds')
        .send({ username: 'user1', password: 'radarr-pass', serviceId: 'radarr' });
      expect(res.status).toBe(200);
      expect(credentialManager.store).toHaveBeenCalledWith('seedhost.password.radarr', 'radarr-pass');
    });

    it('rejects missing username', async () => {
      const { app } = createApp();
      const res = await request(app)
        .post('/api/seedhost-creds')
        .send({ password: 'pass1' });
      expect(res.status).toBeGreaterThanOrEqual(400);
    });
  });

  describe('GET /api/seedhost-creds', () => {
    it('returns credential status with shared password', async () => {
      const credentialManager = {
        store: jest.fn(),
        retrieve: jest.fn().mockImplementation((key) => {
          if (key === 'seedhost.username') return Promise.resolve('user1');
          if (key === 'seedhost.password') return Promise.resolve('pass1');
          return Promise.reject(new Error('not found'));
        }),
        delete: jest.fn(),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app).get('/api/seedhost-creds');
      expect(res.status).toBe(200);
      expect(res.body.hasCredentials).toBe(true);
      expect(res.body.username).toBe('user1');
    });

    it('checks per-service password when serviceId provided', async () => {
      const credentialManager = {
        store: jest.fn(),
        retrieve: jest.fn().mockImplementation((key) => {
          if (key === 'seedhost.username') return Promise.resolve('user1');
          if (key === 'seedhost.password.radarr') return Promise.resolve('radarr-pass');
          return Promise.reject(new Error('not found'));
        }),
        delete: jest.fn(),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app).get('/api/seedhost-creds?serviceId=radarr');
      expect(res.status).toBe(200);
      expect(res.body.hasCredentials).toBe(true);
      expect(res.body.hasPassword).toBe(true);
    });

    it('returns hasCredentials:false when nothing stored', async () => {
      const credentialManager = {
        store: jest.fn(),
        retrieve: jest.fn().mockRejectedValue(new Error('not found')),
        delete: jest.fn(),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app).get('/api/seedhost-creds');
      expect(res.status).toBe(200);
      expect(res.body.hasCredentials).toBe(false);
    });
  });

  describe('DELETE /api/seedhost-creds', () => {
    it('deletes per-service password', async () => {
      const credentialManager = {
        store: jest.fn(),
        retrieve: jest.fn(),
        delete: jest.fn().mockResolvedValue(true),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app).delete('/api/seedhost-creds?serviceId=radarr');
      expect(res.status).toBe(200);
      expect(credentialManager.delete).toHaveBeenCalledWith('seedhost.password.radarr');
    });

    it('deletes all seedhost credentials when no serviceId', async () => {
      const credentialManager = {
        store: jest.fn(),
        retrieve: jest.fn(),
        delete: jest.fn().mockResolvedValue(true),
      };
      const { app } = createApp({ credentialManager });
      const res = await request(app).delete('/api/seedhost-creds');
      expect(res.status).toBe(200);
      expect(credentialManager.delete).toHaveBeenCalledWith('seedhost.username');
      expect(credentialManager.delete).toHaveBeenCalledWith('seedhost.password');
    });
  });

  // ===== SERVICES STATUS ENDPOINT =====

  describe('GET /api/services/status', () => {
    it('returns status for all services', async () => {
      const stateManager = {
        read: jest.fn().mockResolvedValue([
          { id: 'plex', name: 'Plex' },
          { id: 'radarr', name: 'Radarr' },
        ]),
        write: jest.fn(),
        update: jest.fn(),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app).get('/api/services/status');
      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
      expect(res.body).toHaveProperty('checkedAt');
      expect(res.body).toHaveProperty('statuses');
    });

    it('includes internet check in statuses', async () => {
      const stateManager = {
        read: jest.fn().mockResolvedValue([]),
        write: jest.fn(),
        update: jest.fn(),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app).get('/api/services/status');
      expect(res.status).toBe(200);
      expect(res.body.statuses).toHaveProperty('internet');
    });
  });

  // ===== SERVICE UPDATE ENDPOINT =====

  describe('POST /api/services/update', () => {
    it('rejects missing subdomains', async () => {
      const { app } = createApp();
      const res = await request(app)
        .post('/api/services/update')
        .send({ oldSubdomain: 'plex' }); // missing newSubdomain
      expect(res.status).toBeGreaterThanOrEqual(400);
    });

    it('rejects invalid subdomain format', async () => {
      const { app } = createApp();
      const res = await request(app)
        .post('/api/services/update')
        .send({ oldSubdomain: 'INVALID!', newSubdomain: 'plex' });
      expect(res.status).toBeGreaterThanOrEqual(400);
    });

    it('rejects invalid port', async () => {
      const { isValidPort } = require('../../src/security/input-validator');
      isValidPort.mockReturnValue(false);
      const { app } = createApp();
      const res = await request(app)
        .post('/api/services/update')
        .send({ oldSubdomain: 'plex', newSubdomain: 'media', port: 99999 });
      expect(res.status).toBeGreaterThanOrEqual(400);
    });

    it('updates subdomain with DNS and Caddy changes', async () => {
      const caddy = {
        read: jest.fn().mockResolvedValue('plex.sami {\n  reverse_proxy localhost:32400\n}'),
        modify: jest.fn().mockResolvedValue({ success: true }),
        generateConfig: jest.fn().mockReturnValue('media.sami { reverse_proxy localhost:32400 }'),
      };
      const dns = {
        getToken: jest.fn().mockReturnValue('token'),
        call: jest.fn().mockResolvedValue({}),
        createRecord: jest.fn().mockResolvedValue({}),
      };
      const stateManager = {
        read: jest.fn().mockResolvedValue([{ id: 'plex', name: 'Plex' }]),
        write: jest.fn(),
        update: jest.fn(async (fn) => fn([{ id: 'plex', name: 'Plex', url: 'https://plex.sami' }])),
      };
      const { app } = createApp({
        caddy, dns,
        servicesStateManager: stateManager,
      });

      const res = await request(app)
        .post('/api/services/update')
        .send({ oldSubdomain: 'plex', newSubdomain: 'media' });

      expect(res.status).toBe(200);
      expect(res.body.results).toBeDefined();
    });
  });

  // ===== VALIDATION / EDGE CASES =====

  describe('PUT /api/services validation', () => {
    it('rejects services that fail validateServiceConfig', async () => {
      validateServiceConfig.mockImplementation(() => {
        const err = new Error('Bad id format');
        err.errors = ['id contains invalid chars'];
        throw err;
      });
      const { app } = createApp();
      const res = await request(app)
        .put('/api/services')
        .send([{ id: 'bad!id', name: 'Test' }]);
      expect(res.status).toBe(400);
    });
  });

  describe('DELETE /api/services/:id edge cases', () => {
    it('returns 404 when service not in list', async () => {
      const stateManager = {
        read: jest.fn(),
        write: jest.fn(),
        update: jest.fn(async (fn) => fn([{ id: 'radarr' }])),
      };
      const { app } = createApp({ servicesStateManager: stateManager });
      const res = await request(app).delete('/api/services/nonexistent');
      expect(res.status).toBe(404);
    });
  });
});
