# frozen_string_literal: true

require "discourse_js_processor"

RSpec.describe DiscourseJsProcessor do
  describe "should_transpile?" do
    it "returns false for empty strings" do
      expect(DiscourseJsProcessor.should_transpile?(nil)).to eq(false)
      expect(DiscourseJsProcessor.should_transpile?("")).to eq(false)
    end

    it "returns false for a regular js file" do
      expect(DiscourseJsProcessor.should_transpile?("file.js")).to eq(false)
    end

    it "returns true for deprecated .es6 files" do
      expect(DiscourseJsProcessor.should_transpile?("file.es6")).to eq(true)
      expect(DiscourseJsProcessor.should_transpile?("file.js.es6")).to eq(true)
      expect(DiscourseJsProcessor.should_transpile?("file.js.es6.erb")).to eq(true)
    end
  end

  describe "skip_module?" do
    it "returns false for empty strings" do
      expect(DiscourseJsProcessor.skip_module?(nil)).to eq(false)
      expect(DiscourseJsProcessor.skip_module?("")).to eq(false)
    end

    it "returns true if the header is present" do
      expect(DiscourseJsProcessor.skip_module?("// cool comment\n// discourse-skip-module")).to eq(
        true,
      )
    end

    it "returns false if the header is not present" do
      expect(DiscourseJsProcessor.skip_module?("// just some JS\nconsole.log()")).to eq(false)
    end

    it "works end-to-end" do
      source = <<~JS.chomp
        // discourse-skip-module
        console.log("hello world");
      JS
      expect(DiscourseJsProcessor.transpile(source, "test", "test")).to eq(source)
    end
  end

  it "passes through modern JS syntaxes which are supported in our target browsers" do
    script = <<~JS.chomp
      optional?.chaining;
      const template = func`test`;
      let numericSeparator = 100_000_000;
      logicalAssignment ||= 2;
      nullishCoalescing ?? 'works';
      try {
        "optional catch binding";
      } catch {
        "works";
      }
      async function* asyncGeneratorFunction() {
        yield await Promise.resolve('a');
      }
      let a = {
        x,
        y,
        ...spreadRest
      };
    JS

    result = DiscourseJsProcessor.transpile(script, "blah", "blah/mymodule")
    expect(result).to eq <<~JS.strip
      define("blah/mymodule", [], function () {
        "use strict";

      #{script.indent(2)}
      });
    JS
  end

  it "supports decorators and class properties without error" do
    script = <<~JS.chomp
      class MyClass {
        classProperty = 1;
        #privateProperty = 1;
        #privateMethod() {
          console.log("hello world");
        }
        @decorated
        myMethod(){
        }
      }
    JS

    result = DiscourseJsProcessor.transpile(script, "blah", "blah/mymodule")
    expect(result).to include("_applyDecoratedDescriptor")
  end

  it "correctly transpiles widget hbs" do
    result = DiscourseJsProcessor.transpile(<<~JS, "blah", "blah/mymodule")
      import hbs from "discourse/widgets/hbs-compiler";
      const template = hbs`{{somevalue}}`;
    JS
    expect(result).to eq <<~JS.strip
      define("blah/mymodule", [], function () {
        "use strict";

        const template = function (attrs, state) {
          var _r = [];
          _r.push(somevalue);
          return _r;
        };
      });
    JS
  end

  it "correctly transpiles ember hbs" do
    result = DiscourseJsProcessor.transpile(<<~JS, "blah", "blah/mymodule")
      import { hbs } from 'ember-cli-htmlbars';
      const template = hbs`{{somevalue}}`;
    JS
    expect(result).to eq <<~JS.strip
      define("blah/mymodule", ["@ember/template-factory"], function (_templateFactory) {
        "use strict";

        const template = (0, _templateFactory.createTemplateFactory)(
        /*
          {{somevalue}}
        */
        {
          "id": null,
          "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
          "moduleName": "/blah/mymodule",
          "isStrictMode": false
        });
      });
    JS
  end

  describe "Raw template theme transformations" do
    # For the raw templates, we can easily render them serverside, so let's do that

    let(:compiler) { DiscourseJsProcessor::Transpiler.new }
    let(:theme_id) { 22 }

    let(:helpers) { <<~JS }
      Handlebars.registerHelper('theme-prefix', function(themeId, string) {
        return `theme_translations.${themeId}.${string}`
      })
      Handlebars.registerHelper('theme-i18n', function(themeId, string) {
        return `translated(theme_translations.${themeId}.${string})`
      })
      Handlebars.registerHelper('theme-setting', function(themeId, string) {
        return `setting(${themeId}:${string})`
      })
      Handlebars.registerHelper('dummy-helper', function(string) {
        return `dummy(${string})`
      })
    JS

    let(:mini_racer) do
      ctx = MiniRacer::Context.new
      ctx.eval(File.open("#{Rails.root}/node_modules/handlebars/dist/handlebars.js").read)
      ctx.eval(helpers)
      ctx
    end

    def render(template)
      compiled = compiler.compile_raw_template(template, theme_id: theme_id)
      mini_racer.eval "Handlebars.template(#{compiled.squish})({})"
    end

    it "adds the theme id to the helpers" do
      # Works normally
      expect(render("{{theme-prefix 'translation_key'}}")).to eq(
        "theme_translations.22.translation_key",
      )
      expect(render("{{theme-i18n 'translation_key'}}")).to eq(
        "translated(theme_translations.22.translation_key)",
      )
      expect(render("{{theme-setting 'setting_key'}}")).to eq("setting(22:setting_key)")

      # Works when used inside other statements
      expect(render("{{dummy-helper (theme-prefix 'translation_key')}}")).to eq(
        "dummy(theme_translations.22.translation_key)",
      )
    end

    it "doesn't duplicate number parameter inside {{each}}" do
      expect(
        compiler.compile_raw_template(
          "{{#each item as |test test2|}}{{theme-setting 'setting_key'}}{{/each}}",
          theme_id: theme_id,
        ),
      ).to include(
        '{"name":"theme-setting","hash":{},"hashTypes":{},"hashContexts":{},"types":["NumberLiteral","StringLiteral"]',
      )
      # Fail would be if theme-setting is defined with types:["NumberLiteral","NumberLiteral","StringLiteral"]
    end
  end

  describe "Ember template transformations" do
    # For the Ember (Glimmer) templates, serverside rendering is not trivial,
    # so we compile the expected result with the standard compiler and compare to the theme compiler
    let(:theme_id) { 22 }

    def theme_compile(template)
      script = <<~JS
        import { hbs } from 'ember-cli-htmlbars';
        export default hbs(#{template.to_json});
      JS
      result = DiscourseJsProcessor.transpile(script, "", "theme/blah", theme_id: theme_id)
      result.gsub(%r{/\*(.*)\*/}m, "/* (js comment stripped) */")
    end

    def standard_compile(template)
      script = <<~JS
        import { hbs } from 'ember-cli-htmlbars';
        export default hbs(#{template.to_json});
      JS
      result = DiscourseJsProcessor.transpile(script, "", "theme/blah")
      result.gsub(%r{/\*(.*)\*/}m, "/* (js comment stripped) */")
    end

    it "adds the theme id to the helpers" do
      expect(theme_compile "{{theme-prefix 'translation_key'}}").to eq(
        standard_compile "{{theme-prefix #{theme_id} 'translation_key'}}"
      )

      expect(theme_compile "{{theme-i18n 'translation_key'}}").to eq(
        standard_compile "{{theme-i18n #{theme_id} 'translation_key'}}"
      )

      expect(theme_compile "{{theme-setting 'setting_key'}}").to eq(
        standard_compile "{{theme-setting #{theme_id} 'setting_key'}}"
      )

      # Works when used inside other statements
      expect(theme_compile "{{dummy-helper (theme-prefix 'translation_key')}}").to eq(
        standard_compile "{{dummy-helper (theme-prefix #{theme_id} 'translation_key')}}"
      )
    end
  end

  describe "Transpiler#terser" do
    it "can minify code and provide sourcemaps" do
      sources = {
        "multiply.js" => "let multiply = (firstValue, secondValue) => firstValue * secondValue;",
        "add.js" => "let add = (firstValue, secondValue) => firstValue + secondValue;",
      }

      result =
        DiscourseJsProcessor::Transpiler.new.terser(
          sources,
          { sourceMap: { includeSources: true } },
        )
      expect(result.keys).to contain_exactly("code", "decoded_map", "map")

      begin
        # Check the code still works
        ctx = MiniRacer::Context.new
        ctx.eval(result["code"])
        expect(ctx.eval("multiply(2, 3)")).to eq(6)
        expect(ctx.eval("add(2, 3)")).to eq(5)
      ensure
        ctx.dispose
      end

      map = JSON.parse(result["map"])
      expect(map["sources"]).to contain_exactly(*sources.keys)
      expect(map["sourcesContent"]).to contain_exactly(*sources.values)
    end
  end
end