// This file runs before any modules are loaded

// Set test environment variables FIRST (before any module loads)
// Must be at least 32 chars for validation
process.env.NODE_ENV = 'test';
process.env.JWT_SECRET = 'test-jwt-secret-for-testing-purposes-that-is-long-enough';
process.env.COUCHDB_URL = 'http://localhost:5984';
process.env.COUCHDB_DB_NAME = 'test-adopt-a-street';
process.env.PORT = '5001';

// Mock dotenv to prevent .env file from overriding test values
jest.mock('dotenv', () => ({
  config: jest.fn()
}));

// Mock axios first since couchdbService uses it
jest.mock('axios', () => ({
  create: jest.fn(() => ({
    get: jest.fn().mockResolvedValue({ data: {} }),
    put: jest.fn().mockResolvedValue({ data: { ok: true } }),
    post: jest.fn().mockResolvedValue({ data: { ok: true } }),
    delete: jest.fn().mockResolvedValue({ data: { ok: true } }),
  })),
  get: jest.fn().mockResolvedValue({ data: {} }),
  put: jest.fn().mockResolvedValue({ data: { ok: true } }),
  post: jest.fn().mockResolvedValue({ data: { ok: true } }),
  delete: jest.fn().mockResolvedValue({ data: { ok: true } }),
}));

// Mock CouchDB service at the module level to prevent real service from loading
jest.mock('../services/couchdbService', () => {
  const store = new Map(); // _id -> doc
  let connected = true;

  const clone = (o) => (o ? JSON.parse(JSON.stringify(o)) : o);
  const ensureId = (doc) => {
    if (!doc._id) doc._id = `test_${Date.now()}_${Math.random().toString(36).slice(2,8)}`;
    return doc._id;
  };
  const nextRev = (rev) => {
    const n = parseInt((rev || '0').split('-')[0], 10) + 1;
    return `${n}-test`;
  };
  const matchSelector = (doc, selector = {}) => {
    const ops = {
      $gt: (a, b) => a > b,
      $gte: (a, b) => a >= b,
      $in: (a, arr) => Array.isArray(arr) && arr.includes(a),
      $elemMatch: (arr, sub) => Array.isArray(arr) && arr.some((el) => Object.entries(sub).every(([k, v]) => el && el[k] === v)),
    };
    return Object.entries(selector).every(([key, cond]) => {
      // Support nested keys like 'post.postId'
      const value = key.split('.').reduce((acc, k) => (acc ? acc[k] : undefined), doc);
      if (cond && typeof cond === 'object' && !Array.isArray(cond)) {
        return Object.entries(cond).every(([op, cmp]) => {
          if (ops[op]) return ops[op](value, cmp);
          return value && value[op] === cmp; // nested equality
        });
      }
      return value === cond;
    });
  };

  const api = {};

  // Connection
  api.initialize = jest.fn().mockResolvedValue(true);
  api.isReady = jest.fn(() => connected);
  api.isConnected = connected;
  api.isConnecting = false;

  // Basic helpers
  api.getDB = jest.fn(() => ({}));
  api.shutdown = jest.fn().mockResolvedValue(true);
  // Implement validation with type and required fields checks
  api.validateDocument = jest.fn((doc, requiredFields = []) => {
    const errors = [];
    if (!doc || typeof doc !== 'object') {
      errors.push('Document must be an object');
      return errors;
    }
    if (!doc.type || typeof doc.type !== 'string' || doc.type.trim() === '') {
      errors.push('Document must have a valid type');
    }
    if (Array.isArray(requiredFields)) {
      for (const field of requiredFields) {
        if (!(field in doc) || doc[field] === undefined || doc[field] === null || (typeof doc[field] === 'string' && doc[field].trim() === '')) {
          errors.push(`Missing required field: ${field}`);
        }
      }
    }
    return errors;
  });
  api.generateId = jest.fn((type, id) => `${type}_${id}`);
  api.extractOriginalId = jest.fn((prefixedId) => prefixedId.split('_').slice(1).join('_'));

  // CRUD
  api.createDocument = jest.fn(async (doc) => {
    const toSave = clone(doc) || {};
    const id = ensureId(toSave);
    toSave._rev = nextRev(null);
    store.set(id, clone(toSave));
    const saved = clone(toSave);
    // Return both Couch-like fields and convenience fields for tests
    return { ...saved, id, rev: saved._rev };
  });

  api.getDocument = jest.fn(async (id) => clone(store.get(id)) || null);
  // Aliases for compatibility with code/tests
  api.get = api.getDocument;
  api.findDocumentById = api.getDocument;

  api.updateDocument = jest.fn(async (docOrId, maybeDoc) => {
    let doc = maybeDoc ? { ...maybeDoc, _id: docOrId } : docOrId;
    if (!doc || !doc._id) throw new Error('Document must have _id');
    const existing = store.get(doc._id);
    if (!doc._rev) {
      if (!existing) throw new Error('Document not found for update');
      doc._rev = existing._rev;
    }
    const next = { ...existing, ...clone(doc), _rev: nextRev(existing ? existing._rev : doc._rev) };
    store.set(doc._id, clone(next));
    return clone(next);
  });

  api.deleteDocument = jest.fn(async (id /*, rev */) => {
    store.delete(id);
    return { ok: true, id, rev: nextRev('0-test') };
  });
  // Additional alias methods expected by some models/tests
  api.destroy = api.deleteDocument;


  api.create = jest.fn(async (doc) => api.createDocument(doc));
  api.getById = jest.fn(async (id) => api.getDocument(id));
  api.update = jest.fn(async (id, document) => {
    const existing = store.get(id);
    if (!existing) throw new Error('Document not found for update');
    const merged = { ...existing, ...clone(document), _id: id, _rev: existing._rev };
    return api.updateDocument(merged);
  });
  api.delete = jest.fn(async (id) => { store.delete(id); return true; });

  // Query
  api.find = jest.fn(async (queryOrSelector, maybeOptions) => {
    let query;
    if (queryOrSelector && queryOrSelector.selector) query = queryOrSelector;
    else query = { selector: queryOrSelector || {} , ...(maybeOptions || {}) };

    let results = Array.from(store.values()).filter((doc) => matchSelector(doc, query.selector || {}));

    // sort: [{ field: 'asc'|'desc' }]
    if (Array.isArray(query.sort) && query.sort.length > 0) {
      const [sortSpec] = query.sort;
      const [field, order] = Object.entries(sortSpec)[0];
      results.sort((a, b) => {
        const av = field.split('.').reduce((acc, k) => (acc ? acc[k] : undefined), a);
        const bv = field.split('.').reduce((acc, k) => (acc ? acc[k] : undefined), b);
        if (av === bv) return 0;
        const cmp = av > bv ? 1 : -1;
        return order === 'desc' ? -cmp : cmp;
      });
    }

    const skip = Number.isInteger(query.skip) ? query.skip : 0;
    const limit = Number.isInteger(query.limit) ? query.limit : undefined;
    const sliced = limit !== undefined ? results.slice(skip, skip + limit) : results.slice(skip);
    const arr = clone(sliced);
    // Provide both array and .docs shape for compatibility
    arr.docs = arr;
    return arr;
  });

  api.findOne = jest.fn(async (selector) => {
    const docs = await api.find({ selector, limit: 1 });
    return (Array.isArray(docs) ? docs[0] : docs.docs[0]) || null;
  });

  api.findByType = jest.fn(async (type, selector = {}, options = {}) => {
    return api.find({ selector: { type, ...selector }, ...options });
  });

  // Stub badge awarding to avoid unexpected side effects in tests that don't assert it
  api.checkAndAwardBadges = jest.fn().mockResolvedValue([]);


  api.countDocuments = jest.fn(async (selector = {}) => {
    const docs = await api.find({ selector });
    return docs.length;
  });

  api.bulkDocs = jest.fn(async (docsOrPayload) => {
    const payload = Array.isArray(docsOrPayload) ? { docs: docsOrPayload } : (docsOrPayload || { docs: [] });
    const out = [];
    for (const d of payload.docs) {
      if (!d._id || !store.has(d._id)) {
        const created = await api.createDocument(d);
        out.push({ ok: true, id: created._id, rev: created._rev });
      } else {
        const updated = await api.updateDocument(d);
        out.push({ ok: true, id: updated._id, rev: updated._rev });
      }
    }
    return out;
  });

  // User helpers used in code
  api.findUserByEmail = jest.fn(async (email) => {
    const users = await api.find({ selector: { type: 'user', email } });
    return users[0] || null;
  });

  api.findUserById = jest.fn(async (userId) => api.getById(userId));

  api.updateUserPoints = jest.fn(async (userId, pointsChange) => {
    const user = await api.findUserById(userId);
    if (!user) throw new Error('User not found');
    const newPoints = Math.max(0, (user.points || 0) + pointsChange);
    return api.update(userId, { ...user, points: newPoints });
  });

  // Domain helpers used by tests
  api.findStreetsByLocation = jest.fn().mockResolvedValue([]);

  // Testing utility to reset in-memory store
  api.__reset = jest.fn(() => { store.clear(); });

  // Expose mock and reset globally for tests that need direct access
  global.mockCouchdbService = api;
  global.__resetCouchStore = () => { try { store.clear(); } catch (e) {} };

  return api;
}, { virtual: true });

// Mock Cloudinary
jest.mock('cloudinary', () => ({
  v2: {
    config: jest.fn(),
    uploader: {
      upload: jest.fn().mockResolvedValue({
        secure_url: 'https://cloudinary.com/test/image.jpg',
        public_id: 'test_public_id',
        width: 500,
        height: 500,
        format: 'jpg'
      }),
      destroy: jest.fn().mockResolvedValue({ result: 'ok' })
    }
  }
}));