/**
 * Brand Asset routes — PUT /api/brand-assets/:id (metadata edit)
 *
 * TC_566 (rename) and TC_567 (Primary No → Yes) both go through this route:
 * the edit modal posts the whole form body, the route Object.assigns it onto the
 * loaded document and saves. These pin that the two edited fields actually reach
 * the saved document, that the "one primary per type" rule is applied only when
 * the flag is being turned on, and that the response carries the new values back
 * so the table row and the Primary Assets stat can update without a refetch.
 */

import request from 'supertest';
import express from 'express';

const mockBrandAsset: any = {};
mockBrandAsset.findById = jest.fn();
mockBrandAsset.updateMany = jest.fn().mockResolvedValue({ modifiedCount: 0 });

jest.mock('../../models', () => ({
  getModels: () => ({ BrandAsset: mockBrandAsset }),
}));

/** Swapped per-test so the company-scoping branch can be exercised too. */
let currentUser: any = { _id: 'user-1', companyIds: ['company-1'], role: 'member' };

jest.mock('../../middleware/dualAuth', () => ({
  authenticateJwtOrApiToken: (req: any, _res: any, next: any) => {
    req.user = currentUser;
    next();
  },
}));
jest.mock('../../middleware/permissions', () => ({
  requirePermission: () => (_req: any, _res: any, next: any) => next(),
  resolvePermission: () => (_req: any, _res: any, next: any) => next(),
}));

import brandAssetsRouter from '../brandAssets';

/** A stored asset that behaves like the Mongoose doc the route mutates. */
function storedAsset(overrides: Record<string, any> = {}) {
  const doc: any = {
    _id: 'asset-1',
    companyId: 'company-1',
    name: 'Goodluck',
    type: 'logo',
    format: 'png',
    url: '/uploads/brand-assets/goodluck.png',
    source: 'upload',
    tags: [],
    isPrimary: false,
    ...overrides,
  };
  doc.save = jest.fn().mockResolvedValue(doc);
  return doc;
}

/** What the edit modal sends for a metadata-only save. */
function editBody(overrides: Record<string, any> = {}) {
  return {
    name: 'Goodluck',
    type: 'logo',
    description: '',
    url: '/uploads/brand-assets/goodluck.png',
    sourceUrl: '',
    tags: [],
    isPrimary: false,
    format: 'png',
    source: 'url',
    ...overrides,
  };
}

describe('PUT /api/brand-assets/:id — metadata edit', () => {
  let app: express.Application;

  beforeEach(() => {
    app = express();
    app.use(express.json());
    app.use('/api/brand-assets', brandAssetsRouter);
    jest.clearAllMocks();
    mockBrandAsset.updateMany.mockResolvedValue({ modifiedCount: 0 });
    currentUser = { _id: 'user-1', companyIds: ['company-1'], role: 'member' };
  });

  describe('TC_566 — renaming the asset', () => {
    it('persists the new name and returns it', async () => {
      const doc = storedAsset();
      mockBrandAsset.findById.mockResolvedValue(doc);

      const res = await request(app)
        .put('/api/brand-assets/asset-1')
        .send(editBody({ name: 'UpdatedLogo' }))
        .expect(200);

      // Saved, not just echoed back.
      expect(doc.save).toHaveBeenCalledTimes(1);
      expect(doc.name).toBe('UpdatedLogo');
      // The response is what the table row is replaced with.
      expect(res.body.name).toBe('UpdatedLogo');
    });

    it('does not disturb the other stored fields', async () => {
      const doc = storedAsset();
      mockBrandAsset.findById.mockResolvedValue(doc);

      await request(app)
        .put('/api/brand-assets/asset-1')
        .send(editBody({ name: 'UpdatedLogo' }))
        .expect(200);

      expect(doc.type).toBe('logo');
      expect(doc.companyId).toBe('company-1');
      expect(doc.url).toBe('/uploads/brand-assets/goodluck.png');
    });
  });

  describe('TC_567 — toggling Primary from No to Yes', () => {
    it('persists isPrimary and returns it', async () => {
      const doc = storedAsset({ isPrimary: false });
      mockBrandAsset.findById.mockResolvedValue(doc);

      const res = await request(app)
        .put('/api/brand-assets/asset-1')
        .send(editBody({ isPrimary: true }))
        .expect(200);

      expect(doc.save).toHaveBeenCalledTimes(1);
      expect(doc.isPrimary).toBe(true);
      expect(res.body.isPrimary).toBe(true);
    });

    it('demotes the other primary assets of the same type (existing rule)', async () => {
      const doc = storedAsset({ isPrimary: false });
      mockBrandAsset.findById.mockResolvedValue(doc);

      await request(app)
        .put('/api/brand-assets/asset-1')
        .send(editBody({ isPrimary: true }))
        .expect(200);

      expect(mockBrandAsset.updateMany).toHaveBeenCalledWith(
        { companyId: 'company-1', type: 'logo', _id: { $ne: 'asset-1' } },
        { isPrimary: false }
      );
    });

    it('does not touch other assets when primary is left off', async () => {
      const doc = storedAsset({ isPrimary: false });
      mockBrandAsset.findById.mockResolvedValue(doc);

      await request(app)
        .put('/api/brand-assets/asset-1')
        .send(editBody({ name: 'UpdatedLogo' }))
        .expect(200);

      expect(mockBrandAsset.updateMany).not.toHaveBeenCalled();
      expect(doc.isPrimary).toBe(false);
    });

    it('turning primary off does not demote anything', async () => {
      const doc = storedAsset({ isPrimary: true });
      mockBrandAsset.findById.mockResolvedValue(doc);

      await request(app)
        .put('/api/brand-assets/asset-1')
        .send(editBody({ isPrimary: false }))
        .expect(200);

      expect(mockBrandAsset.updateMany).not.toHaveBeenCalled();
      expect(doc.isPrimary).toBe(false);
    });
  });

  describe('failure paths', () => {
    it('404s for an unknown asset without saving', async () => {
      mockBrandAsset.findById.mockResolvedValue(null);

      await request(app)
        .put('/api/brand-assets/missing')
        .send(editBody({ name: 'UpdatedLogo' }))
        .expect(404);
    });

    it('403s when the asset belongs to another company', async () => {
      const doc = storedAsset({ companyId: 'company-other' });
      mockBrandAsset.findById.mockResolvedValue(doc);

      await request(app)
        .put('/api/brand-assets/asset-1')
        .send(editBody({ name: 'UpdatedLogo' }))
        .expect(403);

      expect(doc.save).not.toHaveBeenCalled();
    });

    it('surfaces a validation failure as 400 rather than a silent success', async () => {
      const doc = storedAsset();
      const validationError: any = new Error('Asset name is required');
      validationError.name = 'ValidationError';
      validationError.errors = { name: { message: 'Asset name is required' } };
      doc.save = jest.fn().mockRejectedValue(validationError);
      mockBrandAsset.findById.mockResolvedValue(doc);

      const res = await request(app)
        .put('/api/brand-assets/asset-1')
        .send(editBody({ name: '' }))
        .expect(400);

      expect(res.body.error).toContain('Asset name is required');
    });
  });
});
