# frozen_string_literal: true

RSpec.describe DraftsController do
  fab!(:user)

  describe "#index" do
    it "requires you to be logged in" do
      get "/drafts.json"
      expect(response.status).to eq(403)
    end

    describe "when limit params is invalid" do
      before { sign_in(user) }

      include_examples "invalid limit params", "/drafts.json", described_class::INDEX_LIMIT
    end

    it "returns correct stream length after adding a draft" do
      sign_in(user)
      Draft.set(user, "xxx", 0, "{}")
      get "/drafts.json"
      expect(response.status).to eq(200)
      expect(response.parsed_body["drafts"].length).to eq(1)
    end

    it "has empty stream after deleting last draft" do
      sign_in(user)
      Draft.set(user, "xxx", 0, "{}")
      Draft.clear(user, "xxx", 0)
      get "/drafts.json"
      expect(response.status).to eq(200)
      expect(response.parsed_body["drafts"].length).to eq(0)
    end

    it "does not include topic details when user cannot see topic" do
      topic = Fabricate(:private_message_topic)
      topic_user = topic.user
      other_user = Fabricate(:user)
      Draft.set(topic_user, "topic_#{topic.id}", 0, "{}")
      Draft.set(other_user, "topic_#{topic.id}", 0, "{}")

      sign_in(topic_user)
      get "/drafts.json"
      expect(response.status).to eq(200)
      expect(response.parsed_body["drafts"].first["title"]).to eq(topic.title)

      sign_in(other_user)
      get "/drafts.json"
      expect(response.status).to eq(200)
      expect(response.parsed_body["drafts"].first["title"]).to eq(nil)
    end

    it "returns the translated topic title" do
      viewer = Fabricate(:user, locale: "ja")
      SiteSetting.content_localization_enabled = true
      topic = Fabricate(:topic, locale: "en")
      Fabricate(:topic_localization, topic: topic, locale: "ja", title: "翻訳された題名")
      Draft.set(viewer, "topic_#{topic.id}", 0, "{}")

      sign_in(viewer)
      get "/drafts.json"

      expect(response.parsed_body["drafts"].first["title"]).to eq("翻訳された題名")
    end

    it "omits display user names when names are disabled" do
      SiteSetting.enable_names = false
      topic_author = Fabricate(:user, name: "Hidden Full Name")
      topic = Fabricate(:topic, user: topic_author)
      Draft.set(user, "topic_#{topic.id}", 0, { reply: "draft reply" }.to_json)

      sign_in(user)
      get "/drafts.json"

      draft = response.parsed_body["drafts"].first

      expect(response.status).to eq(200)
      expect(draft).to include("username" => topic_author.username)
      expect(draft).not_to have_key("name")
      expect(response.body).not_to include(topic_author.name)
    end

    it "returns categories when lazy load categories is enabled" do
      SiteSetting.lazy_load_categories_groups = "#{Group::AUTO_GROUPS[:everyone]}"
      category = Fabricate(:category)
      topic = Fabricate(:topic, category: category)
      Draft.set(topic.user, "topic_#{topic.id}", 0, "{}")
      sign_in(topic.user)

      get "/drafts.json"
      expect(response.status).to eq(200)
      draft_keys = response.parsed_body["drafts"].map { |draft| draft["draft_key"] }
      expect(draft_keys).to contain_exactly("topic_#{topic.id}")
      category_ids = response.parsed_body["categories"].map { |cat| cat["id"] }
      expect(category_ids).to contain_exactly(category.id)
    end
  end

  describe "#show" do
    it "returns a draft if requested" do
      sign_in(user)
      Draft.set(user, "hello", 0, "test")

      get "/drafts/hello.json"
      expect(response.status).to eq(200)
      expect(response.parsed_body["draft"]).to eq("test")
    end
  end

  describe "#create" do
    it "requires you to be logged in" do
      post "/drafts.json"
      expect(response.status).to eq(403)
    end

    it "saves a draft" do
      sign_in(user)

      post "/drafts.json", params: { draft_key: "xyz", data: { my: "data" }.to_json, sequence: 0 }

      expect(response.status).to eq(200)
      expect(Draft.get(user, "xyz", 0)).to eq(%q({"my":"data"}))
    end

    it "returns 404 when the key is missing" do
      sign_in(Fabricate(:user))
      post "/drafts.json", params: { data: { my: "data" }.to_json, sequence: 0 }
      expect(response.status).to eq(404)
    end

    it "checks for a raw conflict on update" do
      sign_in(user)
      post = Fabricate(:post, user:)

      post "/drafts.json",
           params: {
             draft_key: "topic",
             sequence: 0,
             data: { postId: post.id, original_text: post.raw, action: "edit" }.to_json,
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["conflict_user"]).to eq(nil)

      post "/drafts.json",
           params: {
             draft_key: "topic",
             sequence: 0,
             data: { postId: post.id, original_text: "something else", action: "edit" }.to_json,
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["conflict_user"]["id"]).to eq(post.last_editor.id)
      expect(response.parsed_body["conflict_user"]).to include("avatar_template")
    end

    it "checks for a title conflict on update" do
      sign_in(user)
      post = Fabricate(:post, user:)

      post "/drafts.json",
           params: {
             draft_key: "topic",
             sequence: 0,
             data: {
               postId: post.id,
               original_text: post.raw,
               original_title: "something else",
               action: "edit",
             }.to_json,
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["conflict_user"]["id"]).to eq(post.last_editor.id)
    end

    it "checks for a tag conflict on update" do
      sign_in(user)
      tag1 = Fabricate(:tag, name: "tag1")
      tag2 = Fabricate(:tag, name: "tag2")
      post = Fabricate(:post, user:)
      # topic has different tags than original_tags in draft
      post.topic.tags = [tag1]

      post "/drafts.json",
           params: {
             draft_key: "topic",
             sequence: 0,
             data: {
               postId: post.id,
               original_text: post.raw,
               original_tags: %w[tag1 tag2],
               action: "edit",
             }.to_json,
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["conflict_user"]["id"]).to eq(post.last_editor.id)
    end

    it "handles hidden tags when checking for tag conflict" do
      sign_in(user)

      regular_tag = Fabricate(:tag)
      admin_only_tag_one = Fabricate(:tag)
      admin_only_tag_two = Fabricate(:tag)

      admin_only_tag_group = Fabricate(:tag_group)
      admin_only_tag_group.tags = [admin_only_tag_one, admin_only_tag_two]
      admin_only_tag_group.permissions = [
        [Group::AUTO_GROUPS[:admins], TagGroupPermission.permission_types[:full]],
      ]
      admin_only_tag_group.save!

      post = Fabricate(:post, user:)
      post.topic.tags = [regular_tag, admin_only_tag_one]

      post "/drafts.json",
           params: {
             draft_key: "topic",
             sequence: 0,
             data: {
               postId: post.id,
               original_text: post.raw,
               original_tags: [regular_tag.name],
               action: "edit",
             }.to_json,
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["conflict_user"]).to eq(nil)
    end

    it "handles tag objects format when checking for tag conflict" do
      sign_in(user)
      tag1 = Fabricate(:tag)
      tag2 = Fabricate(:tag)
      post = Fabricate(:post, user:)
      post.topic.tags = [tag1, tag2]

      post "/drafts.json",
           params: {
             draft_key: "topic",
             sequence: 0,
             data: {
               postId: post.id,
               original_text: post.raw,
               original_tags: [
                 { "id" => tag1.id, "name" => tag1.name, "slug" => tag1.name },
                 { "id" => tag2.id, "name" => tag2.name, "slug" => tag2.name },
               ],
               action: "edit",
             }.to_json,
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["conflict_user"]).to eq(nil)
    end

    it "cant trivially resolve conflicts without interaction" do
      sign_in(user)

      DraftSequence.next!(user, "abc")

      post "/drafts.json",
           params: {
             draft_key: "abc",
             sequence: 0,
             data: { a: "test" }.to_json,
             owner: "abcdefg",
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["draft_sequence"]).to eq(1)
    end

    it "has a clean protocol for ownership handover" do
      sign_in(user)

      post "/drafts.json",
           params: {
             draft_key: "abc",
             sequence: 0,
             data: { a: "test" }.to_json,
             owner: "abcdefg",
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["draft_sequence"]).to eq(0)

      post "/drafts.json",
           params: {
             draft_key: "abc",
             sequence: 0,
             data: { b: "test" }.to_json,
             owner: "hijklmnop",
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["draft_sequence"]).to eq(1)

      expect(DraftSequence.current(user, "abc")).to eq(1)

      post "/drafts.json",
           params: {
             draft_key: "abc",
             sequence: 1,
             data: { c: "test" }.to_json,
             owner: "hijklmnop",
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["draft_sequence"]).to eq(2)

      post "/drafts.json",
           params: {
             draft_key: "abc",
             sequence: 2,
             data: { c: "test" }.to_json,
             owner: "abc",
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body["draft_sequence"]).to eq(3)
    end

    it "raises an error for out-of-sequence draft setting" do
      sign_in(user)
      seq = DraftSequence.next!(user, "abc")
      Draft.set(user, "abc", seq, { b: "test" }.to_json)

      post "/drafts.json",
           params: {
             draft_key: "abc",
             sequence: seq - 1,
             data: { a: "test" }.to_json,
           }

      expect(response.status).to eq(409)

      post "/drafts.json",
           params: {
             draft_key: "abc",
             sequence: seq + 1,
             data: { a: "test" }.to_json,
           }

      expect(response.status).to eq(409)
    end

    context "when data is too big" do
      let(:user) { Fabricate(:user) }
      let(:data) { "a" * (SiteSetting.max_draft_length + 1) }

      before do
        SiteSetting.max_draft_length = 500
        sign_in(user)
      end

      it "returns an error" do
        post "/drafts.json",
             params: {
               draft_key: "xyz",
               data: { reply: data }.to_json,
               sequence: 0,
             }
        expect(response).to have_http_status :bad_request
      end
    end

    context "when data is not too big" do
      context "when data is not proper JSON" do
        let(:user) { Fabricate(:user) }
        let(:data) { "not-proper-json" }

        before { sign_in(user) }

        it "returns an error" do
          post "/drafts.json", params: { draft_key: "xyz", data: data, sequence: 0 }
          expect(response).to have_http_status :bad_request
        end
      end

      context "when data is not a string" do
        before { sign_in(user) }

        it "returns an error" do
          post "/drafts.json", params: { draft_key: "xyz", data: { cat: "tomtom" }, sequence: 0 }
          expect(response).to have_http_status :bad_request
        end
      end
    end

    it "returns 403 when the maximum amount of drafts per users is reached" do
      SiteSetting.max_drafts_per_user = 2

      user1 = Fabricate(:user)
      sign_in(user1)

      data = { my: "data" }.to_json

      # creating the first draft should work
      post "/drafts.json", params: { draft_key: "TOPIC_1", data: data }
      expect(response.status).to eq(200)

      # same draft key, so shouldn't count against the limit
      post "/drafts.json", params: { draft_key: "TOPIC_1", data: data, sequence: 0 }
      expect(response.status).to eq(200)

      # different draft key, so should count against the limit
      post "/drafts.json", params: { draft_key: "TOPIC_2", data: data }
      expect(response.status).to eq(200)

      # limit should be reached now
      post "/drafts.json", params: { draft_key: "TOPIC_3", data: data }
      expect(response.status).to eq(403)

      # updating existing draft should still work
      post "/drafts.json", params: { draft_key: "TOPIC_1", data: data, sequence: 1 }
      expect(response.status).to eq(200)

      # creating a new draft as a different user should still work
      user2 = Fabricate(:user)
      sign_in(user2)
      post "/drafts.json", params: { draft_key: "TOPIC_3", data: data }
      expect(response.status).to eq(200)

      # check the draft counts just to be safe
      expect(Draft.where(user_id: user1.id).count).to eq(2)
      expect(Draft.where(user_id: user2.id).count).to eq(1)
    end

    it "does not leak conflict info for posts user cannot see" do
      private_post = Fabricate(:private_message_post)

      sign_in(user)
      post "/drafts.json",
           params: {
             draft_key: "topic_#{private_post.topic_id}",
             sequence: 0,
             data: {
               postId: private_post.id,
               action: "edit",
               original_text: "wrong text to trigger conflict",
             }.to_json,
           }

      expect(response.status).to eq(200)
      expect(response.parsed_body).not_to have_key("conflict_user")
    end
  end

  describe "#destroy" do
    it "destroys drafts when required" do
      sign_in(user)
      Draft.set(user, "xxx", 0, "hi")
      delete "/drafts/xxx.json", params: { sequence: 0 }

      expect(response.status).to eq(200)
      expect(Draft.get(user, "xxx", 0)).to eq(nil)
    end

    it "denies attempts to destroy unowned draft" do
      sign_in(Fabricate(:admin))
      user = Fabricate(:user)
      Draft.set(user, "xxx", 0, "hi")
      delete "/drafts/xxx.json", params: { sequence: 0, username: user.username }

      # Draft is not deleted because request is not via API
      expect(Draft.get(user, "xxx", 0)).to be_present
    end

    describe "via API" do
      fab!(:admin)
      fab!(:other_user, :user)

      it "admin can target another user with `username` param" do
        api_key = Fabricate(:api_key, user: admin).key
        Draft.set(other_user, "xxx", 0, "hi")

        delete "/drafts/xxx.json",
               params: {
                 sequence: 0,
                 username: other_user.username,
               },
               headers: {
                 HTTP_API_USERNAME: admin.username,
                 HTTP_API_KEY: api_key,
               }

        expect(response.status).to eq(200)
        expect(Draft.get(other_user, "xxx", 0)).to eq(nil)
      end

      it "admin acts on self when `username` param is absent" do
        api_key = Fabricate(:api_key, user: admin).key
        Draft.set(admin, "xxx", 0, "hi")

        delete "/drafts/xxx.json",
               params: {
                 sequence: 0,
               },
               headers: {
                 HTTP_API_USERNAME: admin.username,
                 HTTP_API_KEY: api_key,
               }

        expect(response.status).to eq(200)
        expect(Draft.get(admin, "xxx", 0)).to eq(nil)
      end

      it "non-admin gets 403 when passing `username` to target another user" do
        api_key = Fabricate(:api_key, user: user).key
        Draft.set(user, "xxx", 0, "hi")
        Draft.set(other_user, "xxx", 0, "hi")

        delete "/drafts/xxx.json",
               params: {
                 sequence: 0,
                 username: other_user.username,
               },
               headers: {
                 HTTP_API_USERNAME: user.username,
                 HTTP_API_KEY: api_key,
               }

        expect(response.status).to eq(403)
        expect(Draft.get(user, "xxx", 0)).to be_present
        expect(Draft.get(other_user, "xxx", 0)).to be_present
      end

      it "non-admin acts on self when `username` param is absent" do
        api_key = Fabricate(:api_key, user: user).key
        Draft.set(user, "xxx", 0, "hi")

        delete "/drafts/xxx.json",
               params: {
                 sequence: 0,
               },
               headers: {
                 HTTP_API_USERNAME: user.username,
                 HTTP_API_KEY: api_key,
               }

        expect(response.status).to eq(200)
        expect(Draft.get(user, "xxx", 0)).to eq(nil)
      end

      it "returns 404 when admin targets a nonexistent `username`" do
        api_key = Fabricate(:api_key, user: admin).key

        delete "/drafts/xxx.json",
               params: {
                 sequence: 0,
                 username: "does_not_exist",
               },
               headers: {
                 HTTP_API_USERNAME: admin.username,
                 HTTP_API_KEY: api_key,
               }

        expect(response.status).to eq(404)
      end
    end
  end

  describe "#bulk_destroy" do
    it "requires you to be logged in" do
      delete "/drafts/bulk_destroy.json"
      expect(response.status).to eq(403)
    end

    it "destroys multiple drafts when required" do
      sign_in(user)

      # Create multiple drafts
      Draft.set(user, "draft1", 0, '{"reply": "draft 1 content"}')
      Draft.set(user, "draft2", 0, '{"reply": "draft 2 content"}')
      Draft.set(user, "draft3", 0, '{"reply": "draft 3 content"}')

      expect(Draft.where(user: user).count).to eq(3)

      delete "/drafts/bulk_destroy.json",
             params: {
               draft_keys: %w[draft1 draft2],
               sequences: {
                 "draft1" => 0,
                 "draft2" => 0,
               },
             }

      expect(response.status).to eq(200)
      expect(response.parsed_body["success"]).to eq("OK")
      expect(response.parsed_body["deleted_count"]).to eq(2)

      # Verify drafts were deleted
      expect(Draft.get(user, "draft1", 0)).to eq(nil)
      expect(Draft.get(user, "draft2", 0)).to eq(nil)
      expect(Draft.get(user, "draft3", 0)).to be_present
      expect(Draft.where(user: user).count).to eq(1)
    end

    it "handles empty draft_keys array" do
      sign_in(user)

      delete "/drafts/bulk_destroy.json", params: { draft_keys: [] }

      expect(response.status).to eq(200)
      expect(response.parsed_body["deleted_count"]).to eq(0)
    end

    it "validates sequences and returns error for conflicts" do
      sign_in(user)

      Draft.set(user, "draft1", 0, '{"reply": "draft 1 content"}')
      Draft.set(user, "draft2", 0, '{"reply": "draft 2 content"}')

      delete "/drafts/bulk_destroy.json",
             params: {
               draft_keys: %w[draft1 draft2],
               sequences: {
                 "draft1" => 0,
                 "draft2" => 99,
               }, # Wrong sequence for draft2
             }

      expect(response.status).to eq(409)
      expect(response.parsed_body["failed"]).to eq("FAILED")
      expect(response.parsed_body["errors"]).to include("draft2")

      # Verify no drafts were deleted due to sequence conflict
      expect(Draft.get(user, "draft1", 0)).to be_present
      expect(Draft.get(user, "draft2", 0)).to be_present
    end

    it "handles missing sequences parameter gracefully" do
      sign_in(user)

      Draft.set(user, "draft1", 0, '{"reply": "draft 1 content"}')

      delete "/drafts/bulk_destroy.json", params: { draft_keys: ["draft1"] }

      expect(response.status).to eq(200)
      expect(response.parsed_body["deleted_count"]).to eq(1)
      expect(Draft.get(user, "draft1", 0)).to eq(nil)
    end

    it "requires draft_keys parameter" do
      sign_in(user)

      delete "/drafts/bulk_destroy.json", params: {}

      expect(response.status).to eq(400)
    end

    it "rejects too many draft_keys" do
      sign_in(user)

      keys = (1..31).map { |i| "key_#{i}" }
      delete "/drafts/bulk_destroy.json", params: { draft_keys: keys }

      expect(response.status).to eq(400)
      expect(response.parsed_body["errors"].first).to include(
        I18n.t("draft.bulk_destroy_limit", limit: DraftsController::BULK_DESTROY_LIMIT),
      )
    end

    it "updates user draft count after bulk deletion" do
      sign_in(user)

      # Create multiple drafts
      3.times { |i| Draft.set(user, "draft#{i}", 0, '{"reply": "content"}') }

      initial_draft_count = user.user_stat.draft_count
      expect(initial_draft_count).to be >= 3

      delete "/drafts/bulk_destroy.json",
             params: {
               draft_keys: %w[draft0 draft1 draft2],
               sequences: {
                 "draft0" => 0,
                 "draft1" => 0,
                 "draft2" => 0,
               },
             }

      expect(response.status).to eq(200)

      # Verify user draft count was updated
      user.user_stat.reload
      expect(user.user_stat.draft_count).to eq(initial_draft_count - 3)
    end

    describe "via API" do
      fab!(:admin)

      it "admin can target another user with `username` param" do
        api_key = Fabricate(:api_key, user: admin).key
        Draft.set(user, "draft1", 0, '{"reply": "draft content"}')

        delete "/drafts/bulk_destroy.json",
               params: {
                 draft_keys: ["draft1"],
                 sequences: {
                   "draft1" => 0,
                 },
                 username: user.username,
               },
               headers: {
                 "Api-Key" => api_key,
                 "Api-Username" => admin.username,
               }

        expect(response.status).to eq(200)
        expect(Draft.get(user, "draft1", 0)).to eq(nil)
      end

      it "admin acts on self when `username` param is absent" do
        api_key = Fabricate(:api_key, user: admin).key
        Draft.set(admin, "draft1", 0, '{"reply": "draft content"}')

        delete "/drafts/bulk_destroy.json",
               params: {
                 draft_keys: ["draft1"],
                 sequences: {
                   "draft1" => 0,
                 },
               },
               headers: {
                 "Api-Key" => api_key,
                 "Api-Username" => admin.username,
               }

        expect(response.status).to eq(200)
        expect(Draft.get(admin, "draft1", 0)).to eq(nil)
      end

      it "non-admin gets 403 when passing `username` to target another user" do
        non_admin = Fabricate(:user)
        api_key = Fabricate(:api_key, user: non_admin).key
        Draft.set(user, "draft1", 0, '{"reply": "draft content"}')

        delete "/drafts/bulk_destroy.json",
               params: {
                 draft_keys: ["draft1"],
                 sequences: {
                   "draft1" => 0,
                 },
                 username: user.username,
               },
               headers: {
                 "Api-Key" => api_key,
                 "Api-Username" => non_admin.username,
               }

        expect(response.status).to eq(403)
        expect(Draft.get(user, "draft1", 0)).to be_present
      end
    end
  end
end
