# frozen_string_literal: true

RSpec.describe Theme do
  after { Theme.clear_cache! }

  before { ThemeJavascriptCompiler.disable_terser! }
  after { ThemeJavascriptCompiler.enable_terser! }

  fab! :user do
    Fabricate(:user)
  end

  let(:guardian) { Guardian.new(user) }

  fab!(:theme) { Fabricate(:theme, user: user) }
  let(:child) { Fabricate(:theme, user: user, component: true) }

  it "can properly clean up color schemes" do
    scheme = ColorScheme.create!(theme_id: theme.id, name: "test")
    scheme2 = ColorScheme.create!(theme_id: theme.id, name: "test2")

    Fabricate(:theme, color_scheme_id: scheme2.id)

    theme.destroy!
    scheme2.reload

    expect(scheme2).not_to eq(nil)
    expect(scheme2.theme_id).to eq(nil)
    expect(ColorScheme.find_by(id: scheme.id)).to eq(nil)
  end

  it "can support child themes" do
    child.set_field(target: :common, name: "header", value: "World")
    child.set_field(target: :desktop, name: "header", value: "Desktop")
    child.set_field(target: :mobile, name: "header", value: "Mobile")

    child.save!

    expect(Theme.lookup_field(child.id, :desktop, "header")).to eq("World\nDesktop")
    expect(Theme.lookup_field(child.id, "mobile", :header)).to eq("World\nMobile")

    child.set_field(target: :common, name: "header", value: "Worldie")
    child.save!

    expect(Theme.lookup_field(child.id, :mobile, :header)).to eq("Worldie\nMobile")

    parent = Fabricate(:theme, user: user)

    parent.set_field(target: :common, name: "header", value: "Common Parent")
    parent.set_field(target: :mobile, name: "header", value: "Mobile Parent")

    parent.save!

    parent.add_relative_theme!(:child, child)

    expect(Theme.lookup_field(parent.id, :mobile, "header")).to eq(
      "Common Parent\nMobile Parent\nWorldie\nMobile",
    )
  end

  it "can support parent themes" do
    child.add_relative_theme!(:parent, theme)
    expect(child.parent_themes).to eq([theme])
  end

  it "can automatically disable for mismatching version" do
    theme.create_remote_theme!(remote_url: "", minimum_discourse_version: "99.99.99")
    theme.save!

    expect(Theme.transform_ids(theme.id)).to eq([])
  end

  it "#transform_ids works with nil values" do
    # Used in safe mode
    expect(Theme.transform_ids(nil)).to eq([])
  end

  it "#transform_ids filters out disabled components" do
    theme.add_relative_theme!(:child, child)
    expect(Theme.transform_ids(theme.id)).to eq([theme.id, child.id])
    child.update!(enabled: false)
    expect(Theme.transform_ids(theme.id)).to eq([theme.id])
  end

  it "doesn't allow multi-level theme components" do
    grandchild = Fabricate(:theme, user: user)
    grandparent = Fabricate(:theme, user: user)

    expect do child.add_relative_theme!(:child, grandchild) end.to raise_error(
      Discourse::InvalidParameters,
      I18n.t("themes.errors.no_multilevels_components"),
    )

    expect do grandparent.add_relative_theme!(:child, theme) end.to raise_error(
      Discourse::InvalidParameters,
      I18n.t("themes.errors.no_multilevels_components"),
    )
  end

  it "doesn't allow a child to be user selectable" do
    child.update(user_selectable: true)
    expect(child.errors.full_messages).to contain_exactly(
      I18n.t("themes.errors.component_no_user_selectable"),
    )
  end

  it "doesn't allow a child to be set as the default theme" do
    expect do child.set_default! end.to raise_error(
      Discourse::InvalidParameters,
      I18n.t("themes.errors.component_no_default"),
    )
  end

  it "doesn't allow a component to have color scheme" do
    scheme = ColorScheme.create!(name: "test")
    child.update(color_scheme: scheme)
    expect(child.errors.full_messages).to contain_exactly(
      I18n.t("themes.errors.component_no_color_scheme"),
    )
  end

  it "should correct bad html in body_tag_baked and head_tag_baked" do
    theme.set_field(target: :common, name: "head_tag", value: "<b>I am bold")
    theme.save!

    expect(Theme.lookup_field(theme.id, :desktop, "head_tag")).to eq("<b>I am bold</b>")
  end

  it "should precompile fragments in body and head tags" do
    with_template = <<HTML
    <script type='text/x-handlebars' name='template'>
      {{hello}}
    </script>
    <script type='text/x-handlebars' data-template-name='raw_template.raw'>
      {{hello}}
    </script>
HTML
    theme.set_field(target: :common, name: "header", value: with_template)
    theme.save!

    field = theme.theme_fields.find_by(target_id: Theme.targets[:common], name: "header")
    baked = Theme.lookup_field(theme.id, :mobile, "header")

    expect(baked).to include(field.javascript_cache.url)
    expect(field.javascript_cache.content).to include("@ember/template-factory")
    expect(field.javascript_cache.content).to include("raw-handlebars")
  end

  it "can destroy unbaked theme without errors" do
    with_template = <<HTML
    <script type='text/x-handlebars' name='template'>
      {{hello}}
    </script>
    <script type='text/x-handlebars' data-template-name='raw_template.raw'>
      {{hello}}
    </script>
HTML
    theme.set_field(target: :common, name: "header", value: with_template)
    theme.save!

    field = theme.theme_fields.find_by(target_id: Theme.targets[:common], name: "header")
    baked = Theme.lookup_field(theme.id, :mobile, "header")
    ThemeField.where(id: field.id).update_all(compiler_version: 0) # update_all to avoid callbacks

    field.reload.destroy!
  end

  it "should create body_tag_baked on demand if needed" do
    theme.set_field(target: :common, name: :body_tag, value: "<b>test")
    theme.save

    ThemeField.update_all(value_baked: nil)

    expect(Theme.lookup_field(theme.id, :desktop, :body_tag)).to match(%r{<b>test</b>})
  end

  describe "#switch_to_component!" do
    it "correctly converts a theme to component" do
      theme.add_relative_theme!(:child, child)
      scheme = ColorScheme.create!(name: "test")
      theme.update!(color_scheme_id: scheme.id, user_selectable: true)
      theme.set_default!

      theme.switch_to_component!
      theme.reload

      expect(theme.component).to eq(true)
      expect(theme.user_selectable).to eq(false)
      expect(theme.default?).to eq(false)
      expect(theme.color_scheme_id).to eq(nil)
      expect(ChildTheme.where(parent_theme: theme).exists?).to eq(false)
    end
  end

  describe "#switch_to_theme!" do
    it "correctly converts a component to theme" do
      theme.add_relative_theme!(:child, child)

      child.switch_to_theme!
      theme.reload
      child.reload

      expect(child.component).to eq(false)
      expect(ChildTheme.where(child_theme: child).exists?).to eq(false)
    end
  end

  describe ".transform_ids" do
    let!(:orphan1) { Fabricate(:theme, component: true) }
    let!(:child) { Fabricate(:theme, component: true) }
    let!(:child2) { Fabricate(:theme, component: true) }
    let!(:orphan2) { Fabricate(:theme, component: true) }
    let!(:orphan3) { Fabricate(:theme, component: true) }
    let!(:orphan4) { Fabricate(:theme, component: true) }

    before do
      theme.add_relative_theme!(:child, child)
      theme.add_relative_theme!(:child, child2)
    end

    it "returns an empty array if no ids are passed" do
      expect(Theme.transform_ids(nil)).to eq([])
    end

    it "adds the child themes of the parent" do
      sorted = [child.id, child2.id].sort

      expect(Theme.transform_ids(theme.id)).to eq([theme.id, *sorted])
    end
  end

  describe "plugin api" do
    def transpile(html)
      f =
        ThemeField.create!(
          target_id: Theme.targets[:mobile],
          theme_id: 1,
          name: "after_header",
          value: html,
        )
      f.ensure_baked!
      [f.value_baked, f.javascript_cache, f]
    end

    it "transpiles ES6 code" do
      html = <<HTML
        <script type='text/discourse-plugin' version='0.1'>
          const x = 1;
        </script>
HTML

      baked, javascript_cache, field = transpile(html)
      expect(baked).to include(javascript_cache.url)

      expect(javascript_cache.content).to include("if ('define' in window) {")
      expect(javascript_cache.content).to include(
        "define(\"discourse/theme-#{field.theme_id}/discourse/initializers/theme-field-#{field.id}-mobile-html-script-1\"",
      )
      expect(javascript_cache.content).to include(
        "settings = require(\"discourse/lib/theme-settings-store\").getObjectForTheme(#{field.theme_id});",
      )
      expect(javascript_cache.content).to include(
        "name: \"theme-field-#{field.id}-mobile-html-script-1\",",
      )
      expect(javascript_cache.content).to include("after: \"inject-objects\",")
      expect(javascript_cache.content).to include("(0, _pluginApi.withPluginApi)(\"0.1\", api =>")
      expect(javascript_cache.content).to include("const x = 1;")
    end
  end

  describe "theme upload vars" do
    let :image do
      file_from_fixtures("logo.png")
    end

    it "can handle uploads based of ThemeField" do
      upload = UploadCreator.new(image, "logo.png").create_for(-1)
      theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var)
      theme.set_field(target: :common, name: :scss, value: "body {background-image: url($logo)}")
      theme.save!

      # make sure we do not nuke it
      freeze_time (SiteSetting.clean_orphan_uploads_grace_period_hours + 1).hours.from_now
      Jobs::CleanUpUploads.new.execute(nil)

      expect(Upload.where(id: upload.id)).to be_exists

      # no error for theme field
      theme.reload
      expect(theme.theme_fields.find_by(name: :scss).error).to eq(nil)

      manager = Stylesheet::Manager.new(theme_id: theme.id)

      scss, _map =
        Stylesheet::Manager::Builder.new(
          target: :desktop_theme,
          theme: theme,
          manager: manager,
        ).compile(force: true)

      expect(scss).to include(upload.url)
    end
  end

  describe "theme settings" do
    it "allows values to be used in scss" do
      theme.set_field(
        target: :settings,
        name: :yaml,
        value: "background_color: red\nfont_size: 25px",
      )
      theme.set_field(
        target: :common,
        name: :scss,
        value: "body {background-color: $background_color; font-size: $font-size}",
      )
      theme.save!

      manager = Stylesheet::Manager.new(theme_id: theme.id)

      scss, _map =
        Stylesheet::Manager::Builder.new(
          target: :desktop_theme,
          theme: theme,
          manager: manager,
        ).compile(force: true)

      expect(scss).to include("background-color:red")
      expect(scss).to include("font-size:25px")

      setting = theme.settings.find { |s| s.name == :font_size }
      setting.value = "30px"
      theme.save!

      scss, _map =
        Stylesheet::Manager::Builder.new(
          target: :desktop_theme,
          theme: theme,
          manager: manager,
        ).compile(force: true)

      expect(scss).to include("font-size:30px")

      # Escapes correctly. If not, compiling this would throw an exception
      setting.value = <<~CSS
          \#{$fakeinterpolatedvariable}
          andanothervalue 'withquotes'; margin: 0;
      CSS

      theme.set_field(target: :common, name: :scss, value: "body {font-size: quote($font-size)}")
      theme.save!

      scss, _map =
        Stylesheet::Manager::Builder.new(
          target: :desktop_theme,
          theme: theme,
          manager: manager,
        ).compile(force: true)

      expect(scss).to include(
        'font-size:"#{$fakeinterpolatedvariable}\a andanothervalue \'withquotes\'; margin: 0;\a"',
      )
    end

    it "can use a setting straight away after introducing it" do
      theme.set_field(target: :common, name: :scss, value: "body {background-color: red;}")
      theme.save!

      theme.reload
      theme.set_field(
        target: :settings,
        name: :yaml,
        value: "background_color: red\nfont_size: 25px",
      )
      theme.set_field(
        target: :common,
        name: :scss,
        value: "body {background-color: $background_color;}",
      )
      theme.save!

      expect(
        theme.theme_fields.find_by(target_id: Theme.targets[:common], name: "scss").error,
      ).to eq(nil)
    end

    it "allows values to be used in JS" do
      theme.name = 'awesome theme"'
      theme.set_field(target: :settings, name: :yaml, value: "name: bob")
      theme_field =
        theme.set_field(
          target: :common,
          name: :after_header,
          value:
            '<script type="text/discourse-plugin" version="1.0">alert(settings.name); let a = ()=>{};</script>',
        )
      theme.save!

      theme_field.reload
      expect(Theme.lookup_field(theme.id, :desktop, :after_header)).to include(
        theme_field.javascript_cache.url,
      )
      expect(theme_field.javascript_cache.content).to include("if ('require' in window) {")
      expect(theme_field.javascript_cache.content).to include(
        "require(\"discourse/lib/theme-settings-store\").registerSettings(#{theme_field.theme.id}, {\"name\":\"bob\"});",
      )
      expect(theme_field.javascript_cache.content).to include("if ('define' in window) {")
      expect(theme_field.javascript_cache.content).to include(
        "define(\"discourse/theme-#{theme_field.theme.id}/discourse/initializers/theme-field-#{theme_field.id}-common-html-script-1\",",
      )
      expect(theme_field.javascript_cache.content).to include(
        "name: \"theme-field-#{theme_field.id}-common-html-script-1\",",
      )
      expect(theme_field.javascript_cache.content).to include("after: \"inject-objects\",")
      expect(theme_field.javascript_cache.content).to include(
        "(0, _pluginApi.withPluginApi)(\"1.0\", api =>",
      )
      expect(theme_field.javascript_cache.content).to include("alert(settings.name)")
      expect(theme_field.javascript_cache.content).to include("let a = () => {}")

      setting = theme.settings.find { |s| s.name == :name }
      setting.value = "bill"
      theme.save!

      theme_field.reload
      expect(theme_field.javascript_cache.content).to include(
        "require(\"discourse/lib/theme-settings-store\").registerSettings(#{theme_field.theme.id}, {\"name\":\"bill\"});",
      )
      expect(Theme.lookup_field(theme.id, :desktop, :after_header)).to include(
        theme_field.javascript_cache.url,
      )
    end

    it "is empty when the settings are invalid" do
      theme.set_field(target: :settings, name: :yaml, value: "nil_setting: ")
      theme.save!

      expect(theme.settings).to be_empty
    end
  end

  it "correctly caches theme ids" do
    Theme.where.not(id: theme.id).destroy_all

    theme2 = Fabricate(:theme)

    expect(Theme.theme_ids).to contain_exactly(theme.id, theme2.id)
    expect(Theme.user_theme_ids).to eq([])

    theme.update!(user_selectable: true)

    expect(Theme.user_theme_ids).to contain_exactly(theme.id)

    theme2.update!(user_selectable: true)
    expect(Theme.user_theme_ids).to contain_exactly(theme.id, theme2.id)

    theme.update!(user_selectable: false)
    theme2.update!(user_selectable: false)

    theme.set_default!
    expect(Theme.user_theme_ids).to contain_exactly(theme.id)

    theme.destroy
    theme2.destroy

    expect(Theme.theme_ids).to eq([])
    expect(Theme.user_theme_ids).to eq([])
  end

  it "correctly caches user_themes template" do
    Theme.destroy_all

    json = Site.json_for(guardian)
    user_themes = JSON.parse(json)["user_themes"]
    expect(user_themes).to eq([])

    theme = Fabricate(:theme, name: "bob", user_selectable: true)
    theme.save!

    json = Site.json_for(guardian)
    user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] }
    expect(user_themes).to eq(["bob"])

    theme.name = "sam"
    theme.save!

    json = Site.json_for(guardian)
    user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] }
    expect(user_themes).to eq(["sam"])

    Theme.destroy_all

    json = Site.json_for(guardian)
    user_themes = JSON.parse(json)["user_themes"]
    expect(user_themes).to eq([])
  end

  def cached_settings(id)
    Theme.find_by(id: id).cached_settings.to_json
  end

  def included_settings(id)
    Theme.find_by(id: id).included_settings.to_json
  end

  it "clears color scheme cache correctly" do
    Theme.destroy_all

    cs =
      Fabricate(
        :color_scheme,
        name: "Fancy",
        color_scheme_colors: [
          Fabricate(:color_scheme_color, name: "header_primary", hex: "F0F0F0"),
          Fabricate(:color_scheme_color, name: "header_background", hex: "1E1E1E"),
          Fabricate(:color_scheme_color, name: "tertiary", hex: "858585"),
        ],
      )

    theme =
      Fabricate(:theme, user_selectable: true, user: Fabricate(:admin), color_scheme_id: cs.id)

    theme.set_default!

    expect(ColorScheme.hex_for_name("header_primary")).to eq("F0F0F0")

    Theme.clear_default!

    expect(ColorScheme.hex_for_name("header_primary")).to eq("333333")
  end

  it "correctly notifies about theme changes" do
    cs1 = Fabricate(:color_scheme)
    cs2 = Fabricate(:color_scheme)

    theme = Fabricate(:theme, user_selectable: true, user: user, color_scheme_id: cs1.id)

    messages = MessageBus.track_publish { theme.save! }.filter { |m| m.channel == "/file-change" }
    expect(messages.count).to eq(1)
    expect(messages.first.data.map { |d| d[:target] }).to contain_exactly(
      :desktop_theme,
      :mobile_theme,
    )

    # With color scheme change:
    messages =
      MessageBus
        .track_publish do
          theme.color_scheme_id = cs2.id
          theme.save!
        end
        .filter { |m| m.channel == "/file-change" }
    expect(messages.count).to eq(1)
    expect(messages.first.data.map { |d| d[:target] }).to contain_exactly(
      :admin,
      :desktop,
      :desktop_theme,
      :mobile,
      :mobile_theme,
    )
  end

  it "includes theme_uploads in settings" do
    Theme.where.not(id: theme.id).destroy_all

    upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(-1)
    theme.set_field(type: :theme_upload_var, target: :common, name: "bob", upload_id: upload.id)
    theme.save!

    json = JSON.parse(cached_settings(theme.id))

    expect(json["theme_uploads"]["bob"]).to eq(upload.url)
  end

  it "does not break on missing uploads in settings" do
    Theme.where.not(id: theme.id).destroy_all

    upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(-1)
    theme.set_field(type: :theme_upload_var, target: :common, name: "bob", upload_id: upload.id)
    theme.save!

    Upload.find(upload.id).destroy
    theme.remove_from_cache!

    json = JSON.parse(cached_settings(theme.id))
    expect(json).to be_empty
  end

  it "uses CDN url for theme_uploads in settings" do
    set_cdn_url("http://cdn.localhost")
    Theme.where.not(id: theme.id).destroy_all

    upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(-1)
    theme.set_field(type: :theme_upload_var, target: :common, name: "bob", upload_id: upload.id)
    theme.save!

    json = JSON.parse(cached_settings(theme.id))

    expect(json["theme_uploads"]["bob"]).to eq("http://cdn.localhost#{upload.url}")
  end

  it "uses CDN url for settings of type upload" do
    set_cdn_url("http://cdn.localhost")
    Theme.where.not(id: theme.id).destroy_all

    upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(-1)
    theme.set_field(target: :settings, name: "yaml", value: <<~YAML)
      my_upload:
        type: upload
        default: ""
    YAML

    ThemeSetting.create!(
      theme: theme,
      data_type: ThemeSetting.types[:upload],
      value: upload.id.to_s,
      name: "my_upload",
    )
    theme.save!

    json = JSON.parse(cached_settings(theme.id))
    expect(json["my_upload"]).to eq("http://cdn.localhost#{upload.url}")
  end

  describe "theme translations" do
    it "can list working theme_translation_manager objects" do
      en_translation =
        ThemeField.create!(
          theme_id: theme.id,
          name: "en",
          type_id: ThemeField.types[:yaml],
          target_id: Theme.targets[:translations],
          value: <<~YAML,
        en:
          theme_metadata:
            description: "Description of my theme"
          group_of_translations:
            translation1: en test1
            translation2: en test2
          base_translation1: en test3
          base_translation2: en test4
      YAML
        )
      fr_translation =
        ThemeField.create!(
          theme_id: theme.id,
          name: "fr",
          type_id: ThemeField.types[:yaml],
          target_id: Theme.targets[:translations],
          value: <<~YAML,
        fr:
          group_of_translations:
            translation2: fr test2
          base_translation2: fr test4
          base_translation3: fr test5
      YAML
        )

      I18n.locale = :fr
      theme.update_translation("group_of_translations.translation1", "overriddentest1")
      translations = theme.translations
      theme.reload

      expect(translations.map(&:key)).to eq(
        %w[
          group_of_translations.translation1
          group_of_translations.translation2
          base_translation1
          base_translation2
          base_translation3
        ],
      )

      expect(translations.map(&:default)).to eq(
        ["en test1", "fr test2", "en test3", "fr test4", "fr test5"],
      )

      expect(translations.map(&:value)).to eq(
        ["overriddentest1", "fr test2", "en test3", "fr test4", "fr test5"],
      )
    end

    it "can list internal theme_translation_manager objects" do
      en_translation =
        ThemeField.create!(
          theme_id: theme.id,
          name: "en",
          type_id: ThemeField.types[:yaml],
          target_id: Theme.targets[:translations],
          value: <<~YAML,
        en:
          theme_metadata:
            description: "Description of my theme"
          another_translation: en test4
      YAML
        )
      translations = theme.internal_translations
      expect(translations.map(&:key)).to contain_exactly("theme_metadata.description")
      expect(translations.map(&:value)).to contain_exactly("Description of my theme")
    end

    it "can create a hash of overridden values" do
      en_translation =
        ThemeField.create!(
          theme_id: theme.id,
          name: "en",
          type_id: ThemeField.types[:yaml],
          target_id: Theme.targets[:translations],
          value: <<~YAML,
        en:
          group_of_translations:
            translation1: en test1
      YAML
        )

      theme.update_translation("group_of_translations.translation1", "overriddentest1")
      I18n.locale = :fr
      theme.update_translation("group_of_translations.translation1", "overriddentest2")
      theme.reload
      expect(theme.translation_override_hash).to eq(
        "en" => {
          "group_of_translations" => {
            "translation1" => "overriddentest1",
          },
        },
        "fr" => {
          "group_of_translations" => {
            "translation1" => "overriddentest2",
          },
        },
      )
    end

    it "fall back when listing baked field" do
      theme2 = Fabricate(:theme)

      en_translation =
        ThemeField.create!(
          theme_id: theme.id,
          name: "en",
          type_id: ThemeField.types[:yaml],
          target_id: Theme.targets[:translations],
          value: "",
        )
      fr_translation =
        ThemeField.create!(
          theme_id: theme.id,
          name: "fr",
          type_id: ThemeField.types[:yaml],
          target_id: Theme.targets[:translations],
          value: "",
        )

      en_translation2 =
        ThemeField.create!(
          theme_id: theme2.id,
          name: "en",
          type_id: ThemeField.types[:yaml],
          target_id: Theme.targets[:translations],
          value: "",
        )

      expect(
        Theme.list_baked_fields([theme.id, theme2.id], :translations, "fr").map(&:id),
      ).to contain_exactly(fr_translation.id, en_translation2.id)
    end
  end

  describe "automatic recompile" do
    it "must recompile after bumping theme_field version" do
      child.set_field(target: :common, name: "header", value: "World")
      child.set_field(target: :extra_js, name: "test.js.es6", value: "const hello = 'world';")
      child.save!

      first_common_value = Theme.lookup_field(child.id, :desktop, "header")
      first_extra_js_value = Theme.lookup_field(child.id, :extra_js, nil)

      Theme
        .stubs(:compiler_version)
        .returns("SOME_NEW_HASH") do
          second_common_value = Theme.lookup_field(child.id, :desktop, "header")
          second_extra_js_value = Theme.lookup_field(child.id, :extra_js, nil)

          new_common_compiler_version =
            ThemeField.find_by(theme_id: child.id, name: "header").compiler_version
          new_extra_js_compiler_version =
            ThemeField.find_by(theme_id: child.id, name: "test.js.es6").compiler_version

          expect(first_common_value).to eq(second_common_value)
          expect(first_extra_js_value).to eq(second_extra_js_value)

          expect(new_common_compiler_version).to eq("SOME_NEW_HASH")
          expect(new_extra_js_compiler_version).to eq("SOME_NEW_HASH")
        end
    end

    it "recompiles when the hostname changes" do
      theme.set_field(target: :settings, name: :yaml, value: "name: bob")
      theme_field =
        theme.set_field(
          target: :common,
          name: :after_header,
          value: '<script>console.log("hello world");</script>',
        )
      theme.save!

      expect(Theme.lookup_field(theme.id, :common, :after_header)).to include(
        "_ws=#{Discourse.current_hostname}",
      )

      SiteSetting.force_hostname = "someotherhostname.com"
      Theme.clear_cache!

      expect(Theme.lookup_field(theme.id, :common, :after_header)).to include(
        "_ws=someotherhostname.com",
      )
    end
  end

  describe "extra_scss" do
    let(:scss) { "body { background: red}" }
    let(:second_file_scss) { "p { color: blue};" }
    let(:child_scss) { "body { background: green}" }

    let(:theme) do
      Fabricate(:theme).tap do |t|
        t.set_field(target: :extra_scss, name: "my_files/magic", value: scss)
        t.set_field(target: :extra_scss, name: "my_files/magic2", value: second_file_scss)
        t.save!
      end
    end

    let(:child_theme) do
      Fabricate(:theme).tap do |t|
        t.component = true
        t.set_field(target: :extra_scss, name: "my_files/moremagic", value: child_scss)
        t.save!
        theme.add_relative_theme!(:child, t)
      end
    end

    let(:compiler) do
      manager = Stylesheet::Manager.new(theme_id: theme.id)

      builder =
        Stylesheet::Manager::Builder.new(target: :desktop_theme, theme: theme, manager: manager)

      builder.compile(force: true)
    end

    it "works when importing file by path" do
      theme.set_field(target: :common, name: :scss, value: '@import "my_files/magic";')
      theme.save!

      css, _map = compiler
      expect(css).to include("body{background:red}")
    end

    it "works when importing multiple files" do
      theme.set_field(
        target: :common,
        name: :scss,
        value: '@import "my_files/magic"; @import "my_files/magic2"',
      )
      theme.save!

      css, _map = compiler
      expect(css).to include("body{background:red}")
      expect(css).to include("p{color:blue}")
    end

    it "works for child themes" do
      child_theme.set_field(target: :common, name: :scss, value: '@import "my_files/moremagic"')
      child_theme.save!

      manager = Stylesheet::Manager.new(theme_id: child_theme.id)

      builder =
        Stylesheet::Manager::Builder.new(
          target: :desktop_theme,
          theme: child_theme,
          manager: manager,
        )

      css, _map = builder.compile(force: true)
      expect(css).to include("body{background:green}")
    end
  end

  describe "scss_variables" do
    it "is empty by default" do
      expect(theme.scss_variables).to eq(nil)
    end

    it "includes settings and uploads when set" do
      theme.set_field(
        target: :settings,
        name: :yaml,
        value: "background_color: red\nfont_size: 25px",
      )
      upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(-1)
      theme.set_field(type: :theme_upload_var, target: :common, name: "bobby", upload_id: upload.id)
      theme.save!

      expect(theme.scss_variables).to include("$background_color: unquote(\"red\")")
      expect(theme.scss_variables).to include("$font_size: unquote(\"25px\")")
      expect(theme.scss_variables).to include("$bobby: ")
    end
  end

  describe "#baked_js_tests_with_digest" do
    before do
      ThemeField.create!(
        theme_id: theme.id,
        target_id: Theme.targets[:settings],
        name: "yaml",
        value: "some_number: 1",
      )
      theme.set_field(
        target: :tests_js,
        type: :js,
        name: "acceptance/some-test.js",
        value: "assert.ok(true);",
      )
      theme.save!
    end

    it "returns nil for content and digest if theme does not have tests" do
      ThemeField.destroy_all
      expect(theme.baked_js_tests_with_digest).to eq([nil, nil])
    end

    it "digest does not change when settings are changed" do
      content, digest = theme.baked_js_tests_with_digest
      expect(content).to be_present
      expect(digest).to be_present
      expect(content).to include("assert.ok(true);")

      theme.update_setting(:some_number, 55)
      theme.save!
      expect(theme.build_settings_hash[:some_number]).to eq(55)

      new_content, new_digest = theme.baked_js_tests_with_digest
      expect(new_content).to eq(content)
      expect(new_digest).to eq(digest)
    end
  end

  describe "get_setting" do
    before do
      theme.set_field(target: :settings, name: "yaml", value: <<~YAML)
        enabled:
          type: bool
          default: false
        some_value:
          type: string
          default: "hello"
      YAML

      ThemeSetting.create!(
        theme: theme,
        data_type: ThemeSetting.types[:bool],
        name: "super_feature_enabled",
      )

      theme.save!
    end

    it "returns the value of the setting when given a string represeting the setting name" do
      expect(theme.get_setting("enabled")).to eq(false)
      expect(theme.get_setting("some_value")).to eq("hello")
    end

    it "returns the value of the setting when given a symbol represeting the setting name" do
      expect(theme.get_setting(:enabled)).to eq(false)
      expect(theme.get_setting(:some_value)).to eq("hello")
    end
  end

  describe "#update_setting" do
    it "requests clients to refresh if `refresh: true`" do
      theme.set_field(target: :settings, name: "yaml", value: <<~YAML)
        super_feature_enabled:
          type: bool
          default: false
          refresh: true
      YAML

      ThemeSetting.create!(
        theme: theme,
        data_type: ThemeSetting.types[:bool],
        name: "super_feature_enabled",
      )
      theme.save!

      messages =
        MessageBus
          .track_publish do
            theme.update_setting(:super_feature_enabled, true)
            theme.save!
          end
          .filter { |m| m.channel == "/global/asset-version" }

      expect(messages.count).to eq(1)
    end

    it "does not request clients to refresh if `refresh: false`" do
      theme.set_field(target: :settings, name: "yaml", value: <<~YAML)
        super_feature_enabled:
          type: bool
          default: false
          refresh: false
      YAML

      ThemeSetting.create!(
        theme: theme,
        data_type: ThemeSetting.types[:bool],
        name: "super_feature_enabled",
      )
      theme.save!

      messages =
        MessageBus
          .track_publish do
            theme.update_setting(:super_feature_enabled, true)
            theme.save!
          end
          .filter { |m| m.channel == "/global/asset-version" }

      expect(messages.count).to eq(0)
    end
  end

  describe "#migrate_settings" do
    fab!(:settings_field) { Fabricate(:settings_theme_field, theme: theme, value: <<~YAML) }
        integer_setting: 1
        list_setting: "aa,bb"
      YAML

    fab!(:migration_field) { Fabricate(:migration_theme_field, theme: theme, version: 1) }

    it "persists the results of the last pending migration to the database" do
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          settings.set("integer_setting", 1033);
          settings.set("list_setting", "cc,dd");
          return settings;
        }
      JS

      Fabricate(:migration_theme_field, theme: theme, value: <<~JS, version: 2)
        export default function migrate(settings) {
          settings.set("integer_setting", 9909);
          settings.set("list_setting", "ee,ff");
          return settings;
        }
      JS

      theme.migrate_settings
      expect(theme.get_setting("integer_setting")).to eq(9909)
      expect(theme.get_setting("list_setting")).to eq("ee,ff")
    end

    it "doesn't allow arbitrary settings to be saved in the database" do
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          settings.set("unknown_setting", 8834);
          return settings;
        }
      JS
      expect do theme.migrate_settings end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t(
          "themes.import_error.migrations.unknown_setting_returned_by_migration",
          name: "0001-some-name",
          setting_name: "unknown_setting",
        ),
      )
    end

    it "allows changing a setting's type" do
      theme.update_setting(:list_setting, "zz,aa")
      theme.save!

      setting_record = theme.theme_settings.where(name: "list_setting").first
      expect(setting_record.data_type).to eq(ThemeSetting.types[:string])
      expect(setting_record.value).to eq("zz,aa")

      settings_field.update!(value: <<~YAML)
        integer_setting: 1
        list_setting:
          default: aa|bb
          type: list
      YAML
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          settings.set("list_setting", "zz|aa");
          return settings;
        }
      JS
      theme.reload

      theme.migrate_settings

      expect(theme.theme_settings.where(name: "list_setting").count).to eq(1)
      setting_record = theme.theme_settings.where(name: "list_setting").first

      expect(setting_record.data_type).to eq(ThemeSetting.types[:list])
      expect(setting_record.value).to eq("zz|aa")

      expect(
        theme.theme_settings_migrations.where(theme_field_id: migration_field.id).first.diff,
      ).to eq(
        "additions" => [{ "key" => "list_setting", "val" => "zz|aa" }],
        "deletions" => [{ "key" => "list_setting", "val" => "zz,aa" }],
      )
    end

    it "allows renaming a setting" do
      theme.update_setting(:integer_setting, 11)
      theme.save!

      setting_record = theme.theme_settings.where(name: "integer_setting").first
      expect(setting_record.value).to eq("11")

      settings_field.update!(value: <<~YAML)
        integer_setting_updated: 1
        list_setting: "aa,bb"
      YAML
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          settings.set("integer_setting_updated", settings.get("integer_setting"));
          return settings;
        }
      JS

      theme.reload

      theme.migrate_settings

      expect(theme.theme_settings.where(name: "integer_setting").exists?).to eq(false)

      setting_record = theme.theme_settings.where(name: "integer_setting_updated").first
      expect(setting_record.value).to eq("11")

      expect(
        theme.theme_settings_migrations.where(theme_field_id: migration_field.id).first.diff,
      ).to eq(
        "additions" => [{ "key" => "integer_setting_updated", "val" => 11 }],
        "deletions" => [{ "key" => "integer_setting", "val" => 11 }],
      )
    end

    it "creates a ThemeSettingsMigration record for each migration" do
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          settings.set("integer_setting", 2);
          settings.set("list_setting", "cc,dd");
          return settings;
        }
      JS

      second_migration_field =
        Fabricate(:migration_theme_field, theme: theme, value: <<~JS, version: 2)
        export default function migrate(settings) {
          settings.set("integer_setting", 3);
          settings.set("list_setting", "ee,ff");
          return settings;
        }
      JS

      third_migration_field =
        Fabricate(:migration_theme_field, theme: theme, value: <<~JS, version: 3)
        export default function migrate(settings) {
          settings.set("integer_setting", 4);
          settings.set("list_setting", "gg,hh");
          return settings;
        }
      JS

      theme.migrate_settings

      records = theme.theme_settings_migrations.order(:version)

      expect(records.count).to eq(3)

      expect(records[0].version).to eq(1)
      expect(records[0].name).to eq("some-name")
      expect(records[0].theme_field_id).to eq(migration_field.id)
      expect(records[0].diff).to eq(
        "additions" => [
          { "key" => "integer_setting", "val" => 2 },
          { "key" => "list_setting", "val" => "cc,dd" },
        ],
        "deletions" => [],
      )

      expect(records[1].version).to eq(2)
      expect(records[1].name).to eq("some-name")
      expect(records[1].theme_field_id).to eq(second_migration_field.id)
      expect(records[1].diff).to eq(
        "additions" => [
          { "key" => "integer_setting", "val" => 3 },
          { "key" => "list_setting", "val" => "ee,ff" },
        ],
        "deletions" => [
          { "key" => "integer_setting", "val" => 2 },
          { "key" => "list_setting", "val" => "cc,dd" },
        ],
      )

      expect(records[2].version).to eq(3)
      expect(records[2].name).to eq("some-name")
      expect(records[2].theme_field_id).to eq(third_migration_field.id)
      expect(records[2].diff).to eq(
        "additions" => [
          { "key" => "integer_setting", "val" => 4 },
          { "key" => "list_setting", "val" => "gg,hh" },
        ],
        "deletions" => [
          { "key" => "integer_setting", "val" => 3 },
          { "key" => "list_setting", "val" => "ee,ff" },
        ],
      )
    end

    it "allows removing an old setting that no longer exists" do
      settings_field.update!(value: <<~YAML)
        setting_that_will_be_removed: 1
      YAML
      theme.update_setting(:setting_that_will_be_removed, 1023)
      theme.save!

      settings_field.update!(value: <<~YAML)
        new_setting: 1
      YAML
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          if (settings.get("setting_that_will_be_removed") !== 1023) {
            throw new Error(`expected setting_that_will_be_removed to be 1023, but it was instead ${settings.get("setting_that_will_be_removed")}.`);
          }
          settings.delete("setting_that_will_be_removed");
          return settings;
        }
      JS
      theme.reload
      theme.migrate_settings
      theme.reload

      expect(theme.theme_settings.count).to eq(0)

      records = theme.theme_settings_migrations
      expect(records.size).to eq(1)

      expect(records[0].diff).to eq(
        "additions" => [],
        "deletions" => [{ "key" => "setting_that_will_be_removed", "val" => 1023 }],
      )
    end
  end

  describe "development experience" do
    it "sends 'development-mode-theme-changed event when non-css fields are updated" do
      Theme.any_instance.stubs(:should_refresh_development_clients?).returns(true)

      theme.set_field(target: :common, name: :scss, value: "body {background: green;}")

      messages =
        MessageBus
          .track_publish { theme.save! }
          .filter { |m| m.channel == "/file-change" }
          .map(&:data)

      expect(messages).not_to include("development-mode-theme-changed")

      theme.set_field(target: :common, name: :header, value: "<p>Hello world</p>")

      messages =
        MessageBus
          .track_publish { theme.save! }
          .filter { |m| m.channel == "/file-change" }
          .map(&:data)

      expect(messages).to include(["development-mode-theme-changed"])
    end
  end

  describe "#lookup_field when a theme component is used in multiple themes" do
    fab!(:theme_1) { Fabricate(:theme, user: user) }
    fab!(:theme_2) { Fabricate(:theme, user: user) }
    fab!(:child) { Fabricate(:theme, user: user, component: true) }

    before_all do
      theme_1.add_relative_theme!(:child, child)
      theme_2.add_relative_theme!(:child, child)
    end

    it "efficiently caches fields of theme component by only caching the fields once across multiple themes" do
      child.set_field(target: :common, name: "header", value: "World")
      child.save!

      expect(Theme.lookup_field(theme_1.id, :desktop, "header")).to eq("World")
      expect(Theme.lookup_field(theme_2.id, :desktop, "header")).to eq("World")

      expect(
        Theme.cache.defer_get_set("#{child.id}:common:header:#{Theme.compiler_version}") { raise },
      ).to eq(["World"])
      expect(
        Theme.cache.defer_get_set("#{child.id}:desktop:header:#{Theme.compiler_version}") { raise },
      ).to eq(nil)

      expect(
        Theme
          .cache
          .defer_get_set("#{theme_1.id}:common:header:#{Theme.compiler_version}") { raise },
      ).to eq(nil)
      expect(
        Theme
          .cache
          .defer_get_set("#{theme_1.id}:desktop:header:#{Theme.compiler_version}") { raise },
      ).to eq(nil)

      expect(
        Theme
          .cache
          .defer_get_set("#{theme_2.id}:common:header:#{Theme.compiler_version}") { raise },
      ).to eq(nil)
      expect(
        Theme
          .cache
          .defer_get_set("#{theme_2.id}:desktop:header:#{Theme.compiler_version}") { raise },
      ).to eq(nil)
    end

    it "puts the parent value ahead of the child" do
      theme_1.set_field(target: :common, name: "header", value: "theme_1")
      theme_1.save!

      child.set_field(target: :common, name: "header", value: "child")
      child.save!

      expect(Theme.lookup_field(theme_1.id, :desktop, "header")).to eq("theme_1\nchild")
    end

    it "puts parent translations ahead of child translations" do
      theme_1.set_field(target: :translations, name: "en", value: <<~YAML)
        en:
          theme_1: "test"
      YAML
      theme_1.save!
      theme_field = ThemeField.order(:id).last

      child.set_field(target: :translations, name: "en", value: <<~YAML)
        en:
          child: "test"
      YAML
      child.save!
      child_field = ThemeField.order(:id).last

      expect(theme_field.value_baked).not_to eq(child_field.value_baked)
      expect(Theme.lookup_field(theme_1.id, :translations, :en)).to eq(
        [theme_field, child_field].map(&:value_baked).join("\n"),
      )
    end

    it "prioritizes a locale over its fallback" do
      theme_1.set_field(target: :translations, name: "en", value: <<~YAML)
        en:
          theme_1: "hello"
      YAML
      theme_1.save!
      en_field = ThemeField.order(:id).last

      theme_1.set_field(target: :translations, name: "es", value: <<~YAML)
        es:
          theme_1: "hola"
      YAML
      theme_1.save!
      es_field = ThemeField.order(:id).last

      expect(es_field.value_baked).not_to eq(en_field.value_baked)
      expect(Theme.lookup_field(theme_1.id, :translations, :en)).to eq(en_field.value_baked)
      expect(Theme.lookup_field(theme_1.id, :translations, :es)).to eq(es_field.value_baked)
      expect(Theme.lookup_field(theme_1.id, :translations, :fr)).to eq(en_field.value_baked)
    end
  end
end