# frozen_string_literal: true

RSpec.describe Admin::ThemesController do
  fab!(:admin)
  fab!(:moderator)
  fab!(:user)

  let! :repo do
    setup_git_repo("about.json" => { name: "discourse-branch-header" }.to_json)
  end

  let! :repo_url do
    MockGitImporter.register("https://github.com/discourse/discourse-brand-header.git", repo)
  end

  around(:each) { |group| MockGitImporter.with_mock { group.run } }

  describe "#generate_key_pair" do
    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "can generate key pairs" do
        post "/admin/themes/generate_key_pair.json"
        expect(response.status).to eq(200)
        json = response.parsed_body
        expect(json["private_key"]).to eq(nil)
        expect(json["public_key"]).to include("ssh-rsa ")
        expect(Discourse.redis.get("ssh_key_#{json["public_key"]}")).not_to eq(nil)
      end
    end

    shared_examples "key pair generation not allowed" do
      it "prevents key pair generation with a 404 response" do
        post "/admin/themes/generate_key_pair.json"

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "key pair generation not allowed"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "key pair generation not allowed"
    end
  end

  describe "#upload_asset" do
    let(:file) { file_from_fixtures("fake.woff2", "woff2") }
    let(:filename) { File.basename(file) }
    let(:upload) { Rack::Test::UploadedFile.new(file) }

    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "can create a theme upload" do
        post "/admin/themes/upload_asset.json", params: { file: upload }
        expect(response.status).to eq(201)

        upload = Upload.find_by(original_filename: filename)

        expect(upload.id).not_to be_nil
        expect(response.parsed_body["upload_id"]).to eq(upload.id)
      end

      context "when trying to upload an existing file" do
        let(:uploaded_file) { Upload.find_by(original_filename: filename) }
        let(:response_json) { response.parsed_body }

        before do
          post "/admin/themes/upload_asset.json", params: { file: upload }
          expect(response.status).to eq(201)
        end

        it "reuses the original upload" do
          expect(response.status).to eq(201)
          expect(response_json["upload_id"]).to eq(uploaded_file.id)
        end
      end
    end

    shared_examples "theme asset upload not allowed" do
      it "prevents theme asset upload with a 404 response" do
        expect do
          post "/admin/themes/upload_asset.json", params: { file: upload }
        end.not_to change { Upload.count }

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "theme asset upload not allowed"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "theme asset upload not allowed"
    end
  end

  describe "#export" do
    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "exports correctly" do
        theme = Fabricate(:theme, name: "Awesome Theme")
        theme.set_field(target: :common, name: :scss, value: ".body{color: black;}")
        theme.set_field(target: :desktop, name: :after_header, value: "<b>test</b>")
        theme.set_field(
          target: :extra_js,
          name: "discourse/controller/blah",
          value: 'console.log("test");',
        )
        theme.save!

        get "/admin/customize/themes/#{theme.id}/export"
        expect(response.status).to eq(200)

        # Save the output in a temp file (automatically cleaned up)
        file = Tempfile.new("archive.zip")
        file.write(response.body)
        file.rewind
        uploaded_file = Rack::Test::UploadedFile.new(file.path, "application/zip")

        # Now import it again
        expect do
          post "/admin/themes/import.json", params: { theme: uploaded_file }
          expect(response.status).to eq(201)
        end.to change { Theme.count }.by (1)

        json = response.parsed_body

        expect(json["theme"]["name"]).to eq("Awesome Theme")
        expect(json["theme"]["theme_fields"].length).to eq(3)
      end
    end

    shared_examples "theme export not allowed" do
      it "prevents theme export with a 404 response" do
        theme = Fabricate(:theme, name: "Awesome Theme")

        get "/admin/customize/themes/#{theme.id}/export"

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

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "theme export not allowed"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "theme export not allowed"
    end
  end

  describe "#import" do
    let(:theme_json_file) do
      Rack::Test::UploadedFile.new(
        file_from_fixtures("sam-s-simple-theme.dcstyle.json", "json"),
        "application/json",
      )
    end

    let(:theme_archive) do
      Rack::Test::UploadedFile.new(
        file_from_fixtures("discourse-test-theme.zip", "themes"),
        "application/zip",
      )
    end

    let(:image) { file_from_fixtures("logo.png") }

    context "when logged in as an admin" do
      before { sign_in(admin) }

      context "when theme allowlist mode is enabled" do
        before do
          global_setting :allowed_theme_repos,
                         "https://github.com/discourse/discourse-brand-header.git"
        end

        it "allows allowlisted imports" do
          expect(Theme.allowed_remote_theme_ids.length).to eq(0)

          post "/admin/themes/import.json",
               params: {
                 remote: "    https://github.com/discourse/discourse-brand-header.git       ",
               }

          expect(Theme.allowed_remote_theme_ids.length).to eq(1)
          expect(response.status).to eq(201)
        end

        it "prevents adding disallowed themes" do
          RemoteTheme.stubs(:import_theme)
          remote = "    https://bad.com/discourse/discourse-brand-header.git       "

          post "/admin/themes/import.json", params: { remote: remote }

          expect(response.status).to eq(403)
          expect(response.parsed_body["errors"]).to include(
            I18n.t("themes.import_error.not_allowed_theme", { repo: remote.strip }),
          )
        end

        it "bans json file import" do
          post "/admin/themes/import.json", params: { theme: theme_json_file }
          expect(response.status).to eq(403)
        end
      end

      it "can import a theme from Git" do
        RemoteTheme.stubs(:import_theme)
        post "/admin/themes/import.json",
             params: {
               remote: "    https://github.com/discourse/discourse-brand-header.git       ",
             }

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

      it "responds with suitable error message when a migration fails" do
        repo_path =
          setup_git_repo(
            "about.json" => { name: "test theme" }.to_json,
            "settings.yaml" => "boolean_setting: true",
            "migrations/settings/0001-some-migration.js" => <<~JS,
            export default function migrate(settings) {
              settings.set("unknown_setting", "dsad");
              return settings;
            }
          JS
          )
        repo_url = MockGitImporter.register("https://example.com/initial_repo.git", repo_path)

        post "/admin/themes/import.json", params: { remote: repo_url }

        expect(response.status).to eq(422)
        expect(response.parsed_body["errors"]).to contain_exactly(
          I18n.t(
            "themes.import_error.migrations.unknown_setting_returned_by_migration",
            name: "0001-some-migration",
            setting_name: "unknown_setting",
          ),
        )
      end

      it "fails to import with a failing status" do
        post "/admin/themes/import.json", params: { remote: "non-existent" }

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

      it "fails to import with a failing status" do
        post "/admin/themes/import.json", params: { remote: "https://#{"a" * 10_000}.com" }

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

      it "can lookup a private key by public key" do
        Discourse.redis.setex("ssh_key_abcdef", 1.hour, "rsa private key")

        post "/admin/themes/import.json",
             params: {
               remote: "    #{repo_url}       ",
               public_key: "abcdef",
             }

        expect(RemoteTheme.last.private_key).to eq("rsa private key")

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

      it "imports a theme" do
        post "/admin/themes/import.json", params: { theme: theme_json_file }
        expect(response.status).to eq(201)

        json = response.parsed_body

        expect(json["theme"]["name"]).to eq("Sam's Simple Theme")
        expect(json["theme"]["theme_fields"].length).to eq(2)
        expect(json["theme"]["auto_update"]).to eq(false)
        expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
      end

      it "can fail if theme is not accessible" do
        post "/admin/themes/import.json",
             params: {
               remote: "git@github.com:discourse/discourse-inexistent-theme.git",
             }

        expect(response.status).to eq(422)
        expect(response.parsed_body["errors"]).to contain_exactly(I18n.t("themes.import_error.git"))
      end

      it "can force install theme" do
        post "/admin/themes/import.json",
             params: {
               remote: "git@github.com:discourse/discourse-inexistent-theme.git",
               force: true,
             }

        expect(response.status).to eq(201)
        expect(response.parsed_body["theme"]["name"]).to eq("discourse-inexistent-theme")
      end

      it "fails to import with an error if uploads are not allowed" do
        SiteSetting.theme_authorized_extensions = "nothing"

        expect do post "/admin/themes/import.json", params: { theme: theme_archive } end.to change {
          Theme.count
        }.by (0)

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

      it "imports a theme from an archive" do
        _existing_theme = Fabricate(:theme, name: "Header Icons")

        expect do post "/admin/themes/import.json", params: { theme: theme_archive } end.to change {
          Theme.count
        }.by (1)
        expect(response.status).to eq(201)
        json = response.parsed_body

        expect(json["theme"]["name"]).to eq("Header Icons")
        expect(json["theme"]["theme_fields"].length).to eq(5)
        expect(json["theme"]["auto_update"]).to eq(false)
        expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
      end

      it "updates an existing theme from an archive by id" do
        # Used by theme CLI
        _existing_theme = Fabricate(:theme, name: "Header Icons")
        other_existing_theme = Fabricate(:theme, name: "Some other name")

        messages =
          MessageBus.track_publish do
            expect do
              post "/admin/themes/import.json",
                   params: {
                     bundle: theme_archive,
                     theme_id: other_existing_theme.id,
                   }
            end.to change { Theme.count }.by (0)
          end
        expect(response.status).to eq(201)
        json = response.parsed_body

        # Ensure only one refresh message is sent.
        # More than 1 is wasteful, and can trigger unusual race conditions in the client
        # If this test fails, it probably means `theme.save` is being called twice - check any 'autosave' relations
        file_change_messages = messages.filter { |m| m[:channel] == "/file-change" }
        expect(file_change_messages.count).to eq(1)

        expect(json["theme"]["name"]).to eq("Some other name")
        expect(json["theme"]["id"]).to eq(other_existing_theme.id)
        expect(json["theme"]["theme_fields"].length).to eq(5)
        expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
      end

      it "creates a new theme when id specified as nil" do
        # Used by theme CLI
        existing_theme = Fabricate(:theme, name: "Header Icons")

        expect do
          post "/admin/themes/import.json", params: { bundle: theme_archive, theme_id: nil }
        end.to change { Theme.count }.by (1)
        expect(response.status).to eq(201)
        json = response.parsed_body

        expect(json["theme"]["name"]).to eq("Header Icons")
        expect(json["theme"]["id"]).not_to eq(existing_theme.id)
        expect(json["theme"]["theme_fields"].length).to eq(5)
        expect(json["theme"]["auto_update"]).to eq(false)
        expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
      end
    end

    shared_examples "theme import not allowed" do
      it "prevents theme import with a 404 response" do
        post "/admin/themes/import.json", params: { theme: theme_json_file }

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "theme import not allowed"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "theme import not allowed"
    end
  end

  describe "#index" do
    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "correctly returns themes" do
        ColorScheme.destroy_all
        Theme.destroy_all

        theme = Fabricate(:theme)
        theme.set_field(target: :common, name: :scss, value: ".body{color: black;}")
        theme.set_field(target: :desktop, name: :after_header, value: "<b>test</b>")

        theme.remote_theme =
          RemoteTheme.new(
            remote_url: "awesome.git",
            remote_version: "7",
            local_version: "8",
            remote_updated_at: Time.zone.now,
          )

        theme.save!

        # this will get serialized as well
        ColorScheme.create_from_base(name: "test", colors: [])

        get "/admin/themes.json"

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

        json = response.parsed_body

        expect(json["extras"]["color_schemes"].length).to eq(1)
        theme_json = json["themes"].find { |t| t["id"] == theme.id }
        expect(theme_json["theme_fields"].length).to eq(2)
        expect(theme_json["remote_theme"]["remote_version"]).to eq("7")
      end
    end

    shared_examples "themes inaccessible" do
      it "denies access with a 404 response" do
        get "/admin/themes.json"

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "themes inaccessible"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "themes inaccessible"
    end
  end

  describe "#create" do
    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "creates a theme" do
        post "/admin/themes.json",
             params: {
               theme: {
                 name: "my test name",
                 theme_fields: [name: "scss", target: "common", value: "body{color: red;}"],
               },
             }

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

        json = response.parsed_body

        expect(json["theme"]["theme_fields"].length).to eq(1)
        expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
      end
    end

    shared_examples "theme creation not allowed" do
      it "prevents creation with a 404 response" do
        expect do
          post "/admin/themes.json",
               params: {
                 theme: {
                   name: "my test name",
                   theme_fields: [name: "scss", target: "common", value: "body{color: red;}"],
                 },
               }
        end.not_to change { Theme.count }

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "theme creation not allowed"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "theme creation not allowed"
    end
  end

  describe "#update" do
    let!(:theme) { Fabricate(:theme) }

    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "returns the right response when an invalid id is given" do
        put "/admin/themes/99999.json"

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

      it "can change default theme" do
        SiteSetting.default_theme_id = -1

        put "/admin/themes/#{theme.id}.json", params: { id: theme.id, theme: { default: true } }

        expect(response.status).to eq(200)
        expect(SiteSetting.default_theme_id).to eq(theme.id)
      end

      it "can unset default theme" do
        SiteSetting.default_theme_id = theme.id

        put "/admin/themes/#{theme.id}.json", params: { theme: { default: false } }

        expect(response.status).to eq(200)
        expect(SiteSetting.default_theme_id).to eq(-1)
      end

      context "when theme allowlist mode is enabled" do
        before do
          global_setting :allowed_theme_repos, "  https://magic.com/repo.git, https://x.com/git"
        end

        it "unconditionally bans theme_fields from updating" do
          r = RemoteTheme.create!(remote_url: "https://magic.com/repo.git")
          theme.update!(remote_theme_id: r.id)

          put "/admin/themes/#{theme.id}.json",
              params: {
                theme: {
                  name: "my test name",
                  theme_fields: [
                    { name: "scss", target: "common", value: "" },
                    { name: "scss", target: "desktop", value: "body{color: blue;}" },
                  ],
                },
              }

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

      it "updates a theme" do
        theme.set_field(target: :common, name: :scss, value: ".body{color: black;}")
        theme.save

        child_theme = Fabricate(:theme, component: true)

        upload =
          UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(
            Discourse.system_user.id,
          )

        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                child_theme_ids: [child_theme.id],
                name: "my test name",
                theme_fields: [
                  { name: "scss", target: "common", value: "" },
                  { name: "scss", target: "desktop", value: "body{color: blue;}" },
                  { name: "bob", target: "common", value: "", type_id: 2, upload_id: upload.id },
                ],
              },
            }

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

        json = response.parsed_body

        expect(json["theme"]["theme_fields"].length).to eq(2)
        expect(json["theme"]["child_themes"].length).to eq(1)
        expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
      end

      it "prevents theme update when using ember css selectors" do
        child_theme = Fabricate(:theme, component: true)

        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                child_theme_ids: [child_theme.id],
                name: "my test name",
                theme_fields: [
                  { name: "scss", target: "common", value: "" },
                  { name: "scss", target: "desktop", value: ".ember-view{color: blue;}" },
                ],
              },
            }

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

        json = response.parsed_body

        fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] }
        expect(fields[0]["error"]).to eq(I18n.t("themes.ember_selector_error"))

        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                child_theme_ids: [child_theme.id],
                name: "my test name",
                theme_fields: [
                  { name: "scss", target: "common", value: "" },
                  { name: "scss", target: "desktop", value: "#ember392{color: blue;}" },
                ],
              },
            }

        expect(response.status).to eq(200)
        json = response.parsed_body

        fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] }
        expect(fields[0]["error"]).to eq(I18n.t("themes.ember_selector_error"))
      end

      it "blocks remote theme fields from being locally edited" do
        r = RemoteTheme.create!(remote_url: "https://magic.com/repo.git")
        theme.update!(remote_theme_id: r.id)

        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                theme_fields: [
                  { name: "scss", target: "common", value: "" },
                  { name: "header", target: "common", value: "filename.jpg", upload_id: 4 },
                ],
              },
            }

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

      it "creates new theme fields" do
        expect(theme.theme_fields.count).to eq(0)

        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                theme_fields: [{ name: "scss", target: "common", value: "test" }],
              },
            }

        expect(response.status).to eq(200)
        theme.reload
        expect(theme.theme_fields.count).to eq(1)
        theme_field = theme.theme_fields.first
        expect(theme_field.name).to eq("scss")
        expect(theme_field.target_id).to eq(Theme.targets[:common])
        expect(theme_field.value).to eq("test")
      end

      it "doesn't create theme fields when they don't pass validation" do
        expect(theme.theme_fields.count).to eq(0)

        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                theme_fields: [
                  { name: "scss", target: "common", value: "Na " * 1024**2 + "Batman!" },
                ],
              },
            }

        expect(response.status).to eq(422)
        json = JSON.parse(response.body)
        expect(json["errors"].first).to include("Value is too long")
      end

      it "allows zip-imported theme fields to be locally edited" do
        r = RemoteTheme.create!(remote_url: "")
        theme.update!(remote_theme_id: r.id)

        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                theme_fields: [
                  { name: "scss", target: "common", value: "" },
                  { name: "header", target: "common", value: "filename.jpg", upload_id: 4 },
                ],
              },
            }

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

      it "updates a child theme" do
        child_theme = Fabricate(:theme, component: true)
        put "/admin/themes/#{child_theme.id}.json",
            params: {
              theme: {
                parent_theme_ids: [theme.id],
              },
            }
        expect(child_theme.parent_themes).to eq([theme])
      end

      it "can update translations" do
        theme.set_field(
          target: :translations,
          name: :en,
          value: { en: { somegroup: { somestring: "defaultstring" } } }.deep_stringify_keys.to_yaml,
        )
        theme.save!

        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                translations: {
                  "somegroup.somestring" => "overriddenstring",
                },
              },
            }

        # Response correct
        expect(response.status).to eq(200)
        json = response.parsed_body
        expect(json["theme"]["translations"][0]["value"]).to eq("overriddenstring")

        # Database correct
        theme.reload
        expect(theme.theme_translation_overrides.count).to eq(1)
        expect(theme.theme_translation_overrides.first.translation_key).to eq(
          "somegroup.somestring",
        )

        # Set back to default
        put "/admin/themes/#{theme.id}.json",
            params: {
              theme: {
                translations: {
                  "somegroup.somestring" => "defaultstring",
                },
              },
            }
        # Response correct
        expect(response.status).to eq(200)
        json = response.parsed_body
        expect(json["theme"]["translations"][0]["value"]).to eq("defaultstring")

        # Database correct
        theme.reload
        expect(theme.theme_translation_overrides.count).to eq(0)
      end

      it "checking for updates saves the remote_theme record" do
        theme.remote_theme =
          RemoteTheme.create!(
            remote_url: "http://discourse.org",
            remote_version: "a",
            local_version: "a",
            commits_behind: 0,
          )
        theme.save!
        ThemeStore::GitImporter.any_instance.stubs(:import!)
        ThemeStore::GitImporter.any_instance.stubs(:commits_since).returns(["b", 1])

        put "/admin/themes/#{theme.id}.json", params: { theme: { remote_check: true } }
        theme.reload
        expect(theme.remote_theme.remote_version).to eq("b")
        expect(theme.remote_theme.commits_behind).to eq(1)
      end

      it "can disable component" do
        child = Fabricate(:theme, component: true)

        put "/admin/themes/#{child.id}.json", params: { theme: { enabled: false } }
        expect(response.status).to eq(200)
        json = response.parsed_body
        expect(json["theme"]["enabled"]).to eq(false)
        expect(
          UserHistory.where(
            context: child.id.to_s,
            action: UserHistory.actions[:disable_theme_component],
          ).size,
        ).to eq(1)
        expect(json["theme"]["disabled_by"]["id"]).to eq(admin.id)
      end

      it "enabling/disabling a component creates the correct staff action log" do
        child = Fabricate(:theme, component: true)
        UserHistory.destroy_all

        put "/admin/themes/#{child.id}.json", params: { theme: { enabled: false } }
        expect(response.status).to eq(200)

        expect(
          UserHistory.where(
            context: child.id.to_s,
            action: UserHistory.actions[:disable_theme_component],
          ).size,
        ).to eq(1)
        expect(
          UserHistory.where(
            context: child.id.to_s,
            action: UserHistory.actions[:enable_theme_component],
          ).size,
        ).to eq(0)

        put "/admin/themes/#{child.id}.json", params: { theme: { enabled: true } }
        expect(response.status).to eq(200)
        json = response.parsed_body

        expect(
          UserHistory.where(
            context: child.id.to_s,
            action: UserHistory.actions[:disable_theme_component],
          ).size,
        ).to eq(1)
        expect(
          UserHistory.where(
            context: child.id.to_s,
            action: UserHistory.actions[:enable_theme_component],
          ).size,
        ).to eq(1)

        expect(json["theme"]["disabled_by"]).to eq(nil)
        expect(json["theme"]["enabled"]).to eq(true)
      end

      it "handles import errors on update" do
        theme.create_remote_theme!(remote_url: "https://example.com/repository")
        theme.save!

        # RemoteTheme is extensively tested, and setting up the test scaffold is a large overhead
        # So use a stub here to test the controller
        RemoteTheme
          .any_instance
          .stubs(:update_from_remote)
          .raises(RemoteTheme::ImportError.new("error message"))
        put "/admin/themes/#{theme.id}.json", params: { theme: { remote_update: true } }
        expect(response.status).to eq(422)
        expect(response.parsed_body["errors"].first).to eq("error message")
      end

      it "returns the right error message" do
        theme.update!(component: true)

        put "/admin/themes/#{theme.id}.json", params: { theme: { default: true } }

        expect(response.status).to eq(400)
        expect(response.parsed_body["errors"].first).to include(
          I18n.t("themes.errors.component_no_default"),
        )
      end

      it "prevents converting the default theme to a component" do
        SiteSetting.default_theme_id = theme.id

        put "/admin/themes/#{theme.id}.json", params: { theme: { component: true } }

        # should this error message be localized? InvalidParameters :component
        expect(response.status).to eq(400)
        expect(response.parsed_body["errors"].first).to include("component")
      end
    end

    shared_examples "theme update not allowed" do
      it "prevents updates with a 404 response" do
        SiteSetting.default_theme_id = -1

        put "/admin/themes/#{theme.id}.json", params: { id: theme.id, theme: { default: true } }

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
        expect(SiteSetting.default_theme_id).not_to eq(theme.id)
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "theme update not allowed"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "theme update not allowed"
    end
  end

  describe "#destroy" do
    let!(:theme) { Fabricate(:theme) }

    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "returns the right response when an invalid id is given" do
        delete "/admin/themes/9999.json"

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

      it "deletes the field's javascript cache" do
        theme.set_field(
          target: :common,
          name: :header,
          value: '<script>console.log("test")</script>',
        )
        theme.save!

        javascript_cache =
          theme
            .theme_fields
            .find_by(target_id: Theme.targets[:common], name: :header)
            .javascript_cache
        expect(javascript_cache).to_not eq(nil)

        delete "/admin/themes/#{theme.id}.json"

        expect(response.status).to eq(204)
        expect { theme.reload }.to raise_error(ActiveRecord::RecordNotFound)
        expect { javascript_cache.reload }.to raise_error(ActiveRecord::RecordNotFound)
      end
    end

    shared_examples "theme deletion not allowed" do
      it "prevent deletion with a 404 response" do
        delete "/admin/themes/#{theme.id}.json"

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
        expect(theme.reload).to be_present
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "theme deletion not allowed"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "theme deletion not allowed"
    end
  end

  describe "#preview" do
    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "should return the right response when an invalid id is given" do
        get "/admin/themes/9999/preview.json"

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

    shared_examples "theme previews inaccessible" do
      it "denies access with a 404 response" do
        theme = Fabricate(:theme)

        get "/admin/themes/#{theme.id}/preview.json"

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "theme previews inaccessible"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "theme previews inaccessible"
    end
  end

  describe "#update_single_setting" do
    let(:theme) { Fabricate(:theme) }

    before do
      theme.set_field(target: :settings, name: :yaml, value: "bg: red")
      theme.save!
    end

    context "when logged in as an admin" do
      before { sign_in(admin) }

      it "should update a theme setting" do
        put "/admin/themes/#{theme.id}/setting.json", params: { name: "bg", value: "green" }

        expect(response.status).to eq(200)
        expect(response.parsed_body["bg"]).to eq("green")

        theme.reload
        expect(theme.cached_settings[:bg]).to eq("green")
        user_history = UserHistory.last

        expect(user_history.action).to eq(UserHistory.actions[:change_theme_setting])
      end

      it "should clear a theme setting" do
        put "/admin/themes/#{theme.id}/setting.json", params: { name: "bg" }
        theme.reload

        expect(response.status).to eq(200)
        expect(theme.cached_settings[:bg]).to eq("")
      end
    end

    shared_examples "theme update not allowed" do
      it "prevents updates with a 404 response" do
        put "/admin/themes/#{theme.id}/setting.json", params: { name: "bg", value: "green" }

        expect(response.status).to eq(404)
        expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))

        theme.reload
        expect(theme.cached_settings[:bg]).to eq("red")
      end
    end

    context "when logged in as a moderator" do
      before { sign_in(moderator) }

      include_examples "theme update not allowed"
    end

    context "when logged in as a non-staff user" do
      before { sign_in(user) }

      include_examples "theme update not allowed"
    end
  end

  describe "#bulk_destroy" do
    fab!(:theme) { Fabricate(:theme, name: "Awesome Theme") }
    fab!(:theme_2) { Fabricate(:theme, name: "Another awesome Theme") }
    let(:theme_ids) { [theme.id, theme_2.id] }

    before { sign_in(admin) }

    it "destroys all selected the themes" do
      expect do
        delete "/admin/themes/bulk_destroy.json", params: { theme_ids: theme_ids }
      end.to change { Theme.count }.by(-2)
    end

    it "logs the theme destroy action for each theme" do
      StaffActionLogger.any_instance.expects(:log_theme_destroy).twice
      delete "/admin/themes/bulk_destroy.json", params: { theme_ids: theme_ids }
    end
  end
end