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

// Mock dependencies before requiring the route
jest.mock('../../../models', () => ({
  ForumPost: {
    findAndCountAll: jest.fn(),
    findByPk: jest.fn(),
    findOne: jest.fn(),
    findAll: jest.fn(),
    create: jest.fn(),
  },
  ForumComment: {
    findAll: jest.fn(),
    findByPk: jest.fn(),
    create: jest.fn(),
    count: jest.fn(),
    destroy: jest.fn(),
  },
  PostTag: {
    findAll: jest.fn(),
    findOrCreate: jest.fn(),
    create: jest.fn(),
    destroy: jest.fn(),
  },
  User: {
    findByPk: jest.fn(),
  },
  sequelize: {
    transaction: jest.fn(() => ({
      commit: jest.fn(),
      rollback: jest.fn(),
    })),
  },
}));

jest.mock('sequelize', () => ({
  Op: {
    or: Symbol('or'),
    iLike: Symbol('iLike'),
    in: Symbol('in'),
    ne: Symbol('ne'),
  },
  fn: jest.fn((name, col) => ({ fn: name, col })),
  col: jest.fn((name) => ({ col: name })),
}));

jest.mock('../../../middleware/auth', () => ({
  authenticateToken: (req, res, next) => {
    req.user = { id: 'user-123', role: 'user', isVerified: true };
    next();
  },
  requireAdmin: (req, res, next) => {
    if (req.user && req.user.role === 'admin') {
      next();
    } else {
      res.status(403).json({ error: 'Admin access required' });
    }
  },
  optionalAuth: (req, res, next) => next(),
}));

jest.mock('../../../utils/logger', () => ({
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn(),
  withRequestId: jest.fn(() => ({
    info: jest.fn(),
    error: jest.fn(),
    warn: jest.fn(),
  })),
}));

jest.mock('../../../services/email', () => ({
  forum: {
    sendNewPostNotification: jest.fn().mockResolvedValue(),
    sendNewCommentNotification: jest.fn().mockResolvedValue(),
    sendAnswerAcceptedNotification: jest.fn().mockResolvedValue(),
    sendReplyNotification: jest.fn().mockResolvedValue(),
  },
}));

jest.mock('../../../services/googleMapsService', () => ({
  geocodeAddress: jest.fn().mockResolvedValue({ lat: 40.7128, lng: -74.006 }),
}));

jest.mock('../../../services/locationService', () => ({
  getOrCreateLocation: jest.fn().mockResolvedValue({ id: 'loc-123' }),
}));

jest.mock('../../../utils/s3KeyValidator', () => ({
  validateS3Keys: jest.fn().mockReturnValue({ valid: true }),
}));

jest.mock('../../../config/imageLimits', () => ({
  IMAGE_LIMITS: { forum: 10 },
}));

const { ForumPost, ForumComment, PostTag, User } = require('../../../models');
const forumRoutes = require('../../../routes/forum');

const app = express();
app.use(express.json());
app.use('/forum', forumRoutes);

// Error handler
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

describe('Forum Routes', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('GET /forum/posts', () => {
    it('should return paginated posts', async () => {
      const mockPosts = [
        {
          id: 'post-1',
          title: 'Test Post',
          content: 'Test content',
          category: 'question',
          status: 'open',
          commentCount: 5,
          viewCount: 100,
          author: { id: 'user-1', firstName: 'John', lastName: 'Doe' },
          tags: [{ id: 'tag-1', name: 'javascript' }],
          toJSON: function() { return this; }
        },
      ];

      ForumPost.findAndCountAll.mockResolvedValue({
        count: 1,
        rows: mockPosts,
      });

      const response = await request(app)
        .get('/forum/posts')
        .query({ page: 1, limit: 20 });

      expect(response.status).toBe(200);
      expect(response.body.posts).toHaveLength(1);
      expect(response.body.posts[0].title).toBe('Test Post');
      expect(response.body.totalPages).toBe(1);
      expect(response.body.currentPage).toBe(1);
      expect(response.body.totalPosts).toBe(1);
    });

    it('should filter posts by category', async () => {
      ForumPost.findAndCountAll.mockResolvedValue({
        count: 0,
        rows: [],
      });

      const response = await request(app)
        .get('/forum/posts')
        .query({ category: 'question' });

      expect(response.status).toBe(200);
      expect(ForumPost.findAndCountAll).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            category: 'question',
          }),
        })
      );
    });

    it('should search posts by title and content', async () => {
      ForumPost.findAndCountAll.mockResolvedValue({
        count: 0,
        rows: [],
      });

      const response = await request(app)
        .get('/forum/posts')
        .query({ search: 'javascript' });

      expect(response.status).toBe(200);
      expect(ForumPost.findAndCountAll).toHaveBeenCalled();
    });

    it('should sort posts by different criteria', async () => {
      ForumPost.findAndCountAll.mockResolvedValue({
        count: 0,
        rows: [],
      });

      const response = await request(app)
        .get('/forum/posts')
        .query({ sort: 'comments' });

      expect(response.status).toBe(200);
      expect(ForumPost.findAndCountAll).toHaveBeenCalledWith(
        expect.objectContaining({
          order: expect.arrayContaining([
            ['commentCount', 'DESC'],
          ]),
        })
      );
    });

    it('should handle database errors', async () => {
      ForumPost.findAndCountAll.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .get('/forum/posts');

      expect(response.status).toBe(500);
    });
  });

  describe('GET /forum/posts/:id', () => {
    it('should return a single post with comments', async () => {
      const mockPost = {
        id: 'post-1',
        title: 'Test Post',
        content: 'Test content',
        viewCount: 10,
        isDeleted: false,
        comments: [],
        increment: jest.fn().mockResolvedValue(),
        toJSON: function() {
          const { increment, toJSON, ...rest } = this;
          return rest;
        },
        author: { id: 'user-1', firstName: 'John', lastName: 'Doe', role: 'user' },
        tags: [],
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .get('/forum/posts/post-1');

      expect(response.status).toBe(200);
      expect(response.body.title).toBe('Test Post');
      expect(mockPost.increment).toHaveBeenCalledWith('viewCount', { silent: true });
    });

    it('should return 404 for non-existent post', async () => {
      ForumPost.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .get('/forum/posts/non-existent');

      expect(response.status).toBe(404);
      expect(response.body.error).toBe('Post not found');
    });

    it('should return 404 for deleted post (non-admin)', async () => {
      const mockPost = {
        id: 'post-1',
        isDeleted: true,
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .get('/forum/posts/post-1');

      expect(response.status).toBe(404);
      expect(response.body.error).toBe('Post not found');
    });
  });

  describe('POST /forum/posts', () => {
    const validPostData = {
      title: 'New Forum Post',
      content: 'This is the content of the post',
      category: 'question',
      tags: ['javascript', 'react'],
    };

    it('should create a new post successfully', async () => {
      const mockCreatedPost = {
        id: 'new-post-id',
        title: 'New Forum Post',
        content: 'This is the content of the post',
        category: 'question',
        authorId: 'user-123',
        status: 'open',
      };

      const mockPostWithDetails = {
        ...mockCreatedPost,
        author: { id: 'user-123', firstName: 'John', lastName: 'Doe' },
        tags: [{ id: 'tag-1', tagName: 'javascript' }],
        toJSON: function() { return this; },
      };

      ForumPost.create.mockResolvedValue(mockCreatedPost);
      // After create, findByPk is called to get post with details
      ForumPost.findByPk.mockResolvedValue(mockPostWithDetails);

      const response = await request(app)
        .post('/forum/posts')
        .send(validPostData);

      expect(response.status).toBe(201);
      expect(ForumPost.create).toHaveBeenCalledWith(
        expect.objectContaining({
          title: 'New Forum Post',
          content: 'This is the content of the post',
          category: 'question',
          authorId: 'user-123',
        })
      );
    });

    it('should handle Sequelize validation error for missing title', async () => {
      const validationError = new Error('Validation error');
      validationError.name = 'SequelizeValidationError';
      ForumPost.create.mockRejectedValue(validationError);

      const response = await request(app)
        .post('/forum/posts')
        .send({ content: 'Content without title', category: 'question' });

      expect(response.status).toBe(500);
    });

    it('should handle Sequelize validation error for missing content', async () => {
      const validationError = new Error('Validation error');
      validationError.name = 'SequelizeValidationError';
      ForumPost.create.mockRejectedValue(validationError);

      const response = await request(app)
        .post('/forum/posts')
        .send({ title: 'Title without content', category: 'question' });

      expect(response.status).toBe(500);
    });

    it('should handle Sequelize validation error for missing category', async () => {
      const validationError = new Error('Validation error');
      validationError.name = 'SequelizeValidationError';
      ForumPost.create.mockRejectedValue(validationError);

      const response = await request(app)
        .post('/forum/posts')
        .send({ title: 'Title', content: 'Content' });

      expect(response.status).toBe(500);
    });

    it('should handle Sequelize validation error for invalid category', async () => {
      const validationError = new Error('Validation error');
      validationError.name = 'SequelizeValidationError';
      ForumPost.create.mockRejectedValue(validationError);

      const response = await request(app)
        .post('/forum/posts')
        .send({ title: 'Title', content: 'Content', category: 'invalid' });

      expect(response.status).toBe(500);
    });

    it('should handle Sequelize validation error for title too short', async () => {
      const validationError = new Error('Validation error');
      validationError.name = 'SequelizeValidationError';
      ForumPost.create.mockRejectedValue(validationError);

      const response = await request(app)
        .post('/forum/posts')
        .send({ title: 'Hi', content: 'Content', category: 'question' });

      expect(response.status).toBe(500);
    });
  });

  describe('PUT /forum/posts/:id', () => {
    it('should update own post successfully', async () => {
      const mockPost = {
        id: 'post-1',
        authorId: 'user-123',
        title: 'Original Title',
        content: 'Original content',
        isDeleted: false,
        setTags: jest.fn().mockResolvedValue(),
        update: jest.fn().mockResolvedValue(),
        reload: jest.fn().mockResolvedValue(),
        toJSON: function() { return this; },
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);
      PostTag.findOrCreate.mockResolvedValue([{ id: 'tag-1', name: 'updated' }]);

      const response = await request(app)
        .put('/forum/posts/post-1')
        .send({ title: 'Updated Title', content: 'Updated content' });

      expect(response.status).toBe(200);
      expect(mockPost.update).toHaveBeenCalledWith(
        expect.objectContaining({
          title: 'Updated Title',
          content: 'Updated content',
        })
      );
    });

    it('should return 404 for non-existent post', async () => {
      ForumPost.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .put('/forum/posts/non-existent')
        .send({ title: 'Updated' });

      expect(response.status).toBe(404);
    });

    it('should return 403 when updating other users post', async () => {
      const mockPost = {
        id: 'post-1',
        authorId: 'other-user',
        isDeleted: false,
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .put('/forum/posts/post-1')
        .send({ title: 'Updated' });

      expect(response.status).toBe(403);
      expect(response.body.error).toBe('Unauthorized');
    });
  });

  describe('DELETE /forum/posts/:id', () => {
    it('should hard delete own post', async () => {
      const mockPost = {
        id: 'post-1',
        authorId: 'user-123',
        isDeleted: false,
        destroy: jest.fn().mockResolvedValue(),
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .delete('/forum/posts/post-1');

      expect(response.status).toBe(204);
      expect(mockPost.destroy).toHaveBeenCalled();
    });

    it('should return 404 for non-existent post', async () => {
      ForumPost.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .delete('/forum/posts/non-existent');

      expect(response.status).toBe(404);
    });

    it('should return 403 when deleting other users post', async () => {
      const mockPost = {
        id: 'post-1',
        authorId: 'other-user',
        isDeleted: false,
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .delete('/forum/posts/post-1');

      expect(response.status).toBe(403);
    });
  });

  describe('POST /forum/posts/:id/comments', () => {
    it('should add a comment to a post', async () => {
      const mockPost = {
        id: 'post-1',
        authorId: 'post-author',
        isDeleted: false,
        status: 'open',
        increment: jest.fn().mockResolvedValue(),
        update: jest.fn().mockResolvedValue(),
      };

      const mockCreatedComment = {
        id: 'comment-1',
        content: 'Great post!',
        authorId: 'user-123',
        postId: 'post-1',
      };

      const mockCommentWithDetails = {
        ...mockCreatedComment,
        author: { id: 'user-123', firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
        toJSON: function() { return this; },
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);
      ForumComment.create.mockResolvedValue(mockCreatedComment);
      // After create, findByPk is called to get comment with details
      ForumComment.findByPk.mockResolvedValue(mockCommentWithDetails);

      const response = await request(app)
        .post('/forum/posts/post-1/comments')
        .send({ content: 'Great post!' });

      expect(response.status).toBe(201);
      expect(ForumComment.create).toHaveBeenCalledWith(
        expect.objectContaining({
          content: 'Great post!',
          authorId: 'user-123',
          postId: 'post-1',
        })
      );
      expect(mockPost.increment).toHaveBeenCalledWith('commentCount');
    });

    it('should handle Sequelize validation error for missing content', async () => {
      const mockPost = {
        id: 'post-1',
        status: 'open',
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const validationError = new Error('Validation error');
      validationError.name = 'SequelizeValidationError';
      ForumComment.create.mockRejectedValue(validationError);

      const response = await request(app)
        .post('/forum/posts/post-1/comments')
        .send({});

      expect(response.status).toBe(500);
    });

    it('should return 404 for non-existent post', async () => {
      ForumPost.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post('/forum/posts/non-existent/comments')
        .send({ content: 'Comment' });

      expect(response.status).toBe(404);
    });

    it('should return 403 when commenting on closed post', async () => {
      const mockPost = {
        id: 'post-1',
        isDeleted: false,
        status: 'closed',
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .post('/forum/posts/post-1/comments')
        .send({ content: 'Comment' });

      expect(response.status).toBe(403);
      expect(response.body.error).toContain('closed');
    });

    it('should support replying to another comment', async () => {
      const mockPost = {
        id: 'post-1',
        authorId: 'post-author',
        isDeleted: false,
        status: 'open',
        increment: jest.fn().mockResolvedValue(),
        update: jest.fn().mockResolvedValue(),
      };

      const mockParentComment = {
        id: 'parent-comment',
        postId: 'post-1',
        authorId: 'other-user',
        isDeleted: false,
      };

      const mockCreatedReply = {
        id: 'reply-1',
        content: 'Reply to comment',
        parentCommentId: 'parent-comment',
        authorId: 'user-123',
        postId: 'post-1',
      };

      const mockReplyWithDetails = {
        ...mockCreatedReply,
        author: { id: 'user-123', firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
        toJSON: function() { return this; },
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);
      // First findByPk call checks parent comment, second gets created comment with details
      ForumComment.findByPk
        .mockResolvedValueOnce(mockParentComment)
        .mockResolvedValueOnce(mockReplyWithDetails);
      ForumComment.create.mockResolvedValue(mockCreatedReply);

      const response = await request(app)
        .post('/forum/posts/post-1/comments')
        .send({ content: 'Reply to comment', parentCommentId: 'parent-comment' });

      expect(response.status).toBe(201);
      expect(ForumComment.create).toHaveBeenCalledWith(
        expect.objectContaining({
          parentCommentId: 'parent-comment',
        })
      );
    });
  });

  describe('GET /forum/my-posts', () => {
    it('should return authenticated users posts', async () => {
      const mockPosts = [
        {
          id: 'post-1',
          title: 'My Post',
          authorId: 'user-123',
          toJSON: function() { return this; },
        },
      ];

      ForumPost.findAll.mockResolvedValue(mockPosts);

      const response = await request(app)
        .get('/forum/my-posts');

      expect(response.status).toBe(200);
      expect(response.body).toHaveLength(1);
      expect(ForumPost.findAll).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            authorId: 'user-123',
          }),
        })
      );
    });
  });

  describe('GET /forum/tags', () => {
    it('should return all tags', async () => {
      const mockTags = [
        { tagName: 'javascript', count: 10 },
        { tagName: 'react', count: 5 },
      ];

      PostTag.findAll.mockResolvedValue(mockTags);

      const response = await request(app)
        .get('/forum/tags');

      expect(response.status).toBe(200);
      expect(response.body).toHaveLength(2);
      expect(response.body[0].tagName).toBe('javascript');
    });
  });

  describe('PATCH /forum/posts/:id/status', () => {
    it('should update post status by author', async () => {
      const mockPost = {
        id: 'post-1',
        authorId: 'user-123',
        status: 'open',
        isDeleted: false,
        update: jest.fn().mockResolvedValue(),
        reload: jest.fn().mockResolvedValue(),
        toJSON: function() { return this; },
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .patch('/forum/posts/post-1/status')
        .send({ status: 'answered' });

      expect(response.status).toBe(200);
      expect(mockPost.update).toHaveBeenCalledWith({
        status: 'answered',
        closedBy: null,
        closedAt: null,
      });
    });

    it('should reject invalid status', async () => {
      const mockPost = {
        id: 'post-1',
        authorId: 'user-123',
        isDeleted: false,
      };

      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .patch('/forum/posts/post-1/status')
        .send({ status: 'invalid-status' });

      expect(response.status).toBe(400);
      expect(response.body.error).toBe('Invalid status value');
    });
  });

  describe('PUT /forum/comments/:id', () => {
    it('should update own comment', async () => {
      const mockComment = {
        id: 'comment-1',
        authorId: 'user-123',
        postId: 'post-1',
        content: 'Original',
        isDeleted: false,
        post: { id: 'post-1', isDeleted: false },
        update: jest.fn().mockResolvedValue(),
        reload: jest.fn().mockResolvedValue(),
        toJSON: function() { return this; },
      };

      ForumComment.findByPk.mockResolvedValue(mockComment);

      const response = await request(app)
        .put('/forum/comments/comment-1')
        .send({ content: 'Updated content' });

      expect(response.status).toBe(200);
      expect(mockComment.update).toHaveBeenCalledWith(
        expect.objectContaining({
          content: 'Updated content',
        })
      );
    });

    it('should return 403 when editing other users comment', async () => {
      const mockComment = {
        id: 'comment-1',
        authorId: 'other-user',
        isDeleted: false,
        post: { id: 'post-1', isDeleted: false },
      };

      ForumComment.findByPk.mockResolvedValue(mockComment);

      const response = await request(app)
        .put('/forum/comments/comment-1')
        .send({ content: 'Updated' });

      expect(response.status).toBe(403);
    });

    it('should return 404 for non-existent comment', async () => {
      ForumComment.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .put('/forum/comments/non-existent')
        .send({ content: 'Updated' });

      expect(response.status).toBe(404);
    });
  });

  describe('DELETE /forum/comments/:id', () => {
    it('should soft delete own comment', async () => {
      const mockComment = {
        id: 'comment-1',
        authorId: 'user-123',
        postId: 'post-1',
        isDeleted: false,
        update: jest.fn().mockResolvedValue(),
      };

      const mockPost = {
        id: 'post-1',
        commentCount: 5,
        decrement: jest.fn().mockResolvedValue(),
      };

      ForumComment.findByPk.mockResolvedValue(mockComment);
      ForumPost.findByPk.mockResolvedValue(mockPost);

      const response = await request(app)
        .delete('/forum/comments/comment-1');

      // Returns 204 No Content on successful delete
      expect(response.status).toBe(204);
      expect(mockComment.update).toHaveBeenCalledWith({ isDeleted: true });
      expect(mockPost.decrement).toHaveBeenCalledWith('commentCount');
    });

    it('should return 404 for non-existent comment', async () => {
      ForumComment.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .delete('/forum/comments/non-existent');

      expect(response.status).toBe(404);
    });

    it('should return 403 when deleting other users comment', async () => {
      const mockComment = {
        id: 'comment-1',
        authorId: 'other-user',
        isDeleted: false,
      };

      ForumComment.findByPk.mockResolvedValue(mockComment);

      const response = await request(app)
        .delete('/forum/comments/comment-1');

      expect(response.status).toBe(403);
    });
  });
});
