# frozen_string_literal: true

RSpec.describe EmbedController do
  let(:embed_url) { "http://eviltrout.com/2013/02/10/why-discourse-uses-emberjs.html" }
  let(:embed_url_secure) { "https://eviltrout.com/2013/02/10/why-discourse-uses-emberjs.html" }

  fab!(:topic)

  describe "#info" do
    context "without api key" do
      it "returns an embed error without an API key" do
        get "/embed/info.json"

        expect(response.body).to match(I18n.t("embed.error"))
      end
    end

    context "with api key" do
      let(:api_key) { Fabricate(:api_key) }

      context "with valid embed url" do
        let(:topic_embed) { Fabricate(:topic_embed, embed_url: embed_url) }

        it "returns information about the topic" do
          get "/embed/info.json",
              params: {
                embed_url: topic_embed.embed_url,
              },
              headers: {
                HTTP_API_KEY: api_key.key,
                HTTP_API_USERNAME: "system",
              }

          expect(response.parsed_body["topic_id"]).to eq(topic_embed.topic.id)
          expect(response.parsed_body["post_id"]).to eq(topic_embed.post.id)
          expect(response.parsed_body["topic_slug"]).to eq(topic_embed.topic.slug)
        end

        it "returns not found for topics the API user cannot see" do
          user = Fabricate(:user)
          api_key = Fabricate(:api_key, user: user)
          restricted_category = Fabricate(:category)
          restricted_category.set_permissions(staff: :full)
          restricted_category.save!
          restricted_topic = Fabricate(:topic, category: restricted_category, posts_count: 5)
          restricted_topic_embed =
            Fabricate(
              :topic_embed,
              post: Fabricate(:post, topic: restricted_topic),
              topic: restricted_topic,
              embed_url: "http://eviltrout.com/private-article",
            )

          get "/embed/info.json",
              params: {
                embed_url: restricted_topic_embed.embed_url,
              },
              headers: {
                HTTP_API_KEY: api_key.key,
                HTTP_API_USERNAME: user.username,
              }

          expect(response.status).to eq(404)
          expect(response.parsed_body["error_type"]).to eq("not_found")
          expect(response.body).not_to include(restricted_topic.slug)
        end
      end

      context "without invalid embed url" do
        it "returns error response" do
          get "/embed/info.json",
              params: {
                embed_url: "http://nope.com",
              },
              headers: {
                HTTP_API_KEY: api_key.key,
                HTTP_API_USERNAME: "system",
              }

          json = response.parsed_body
          expect(json["error_type"]).to eq("not_found")
        end
      end
    end
  end

  describe "#topics" do
    it "raises an error when not enabled" do
      get "/embed/topics?embed_id=de-1234"

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

    context "when enabled" do
      before { SiteSetting.embed_topics_list = true }

      it "raises an error with a weird id" do
        get "/embed/topics?discourse_embed_id=../asdf/-1234", headers: headers

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

      it "returns a list of topics" do
        get "/embed/topics?discourse_embed_id=de-1234",
            headers: {
              "REFERER" => "https://example.com/evil-trout",
            }

        expect(response.status).to eq(200)
        expect(response.headers["X-Frame-Options"]).to be_nil
        expect(response.body).to match("data-embed-id=\"de-1234\"")
        expect(response.body).to match("data-topic-id=\"#{topic.id}\"")
        expect(response.body).to match("data-referer=\"https://example.com/evil-trout\"")
      end

      it "ignores invalid path allowlists from legacy hosts" do
        embeddable_host = Fabricate(:embeddable_host, allowed_paths: "/articles/.*")
        embeddable_host.update_column(:allowed_paths, "[invalid")

        get "/embed/topics?discourse_embed_id=de-1234",
            headers: {
              "REFERER" => "https://#{embeddable_host.host}/articles/test",
            }

        expect(response.status).to eq(200)
        expect(response.body).to match("data-embed-id=\"de-1234\"")
      end

      it "returns a list of top topics" do
        good_topic = Fabricate(:topic, like_count: 1000, posts_count: 100)
        TopTopic.refresh!

        get "/embed/topics?discourse_embed_id=de-1234&top_period=yearly",
            headers: {
              "REFERER" => "https://example.com/evil-trout",
            }
        expect(response.status).to eq(200)
        expect(response.headers["X-Frame-Options"]).to be_nil
        expect(response.body).to match("data-embed-id=\"de-1234\"")
        expect(response.body).to match("data-topic-id=\"#{good_topic.id}\"")
        expect(response.body).not_to match("data-topic-id=\"#{topic.id}\"")
        expect(response.body).to match("data-referer=\"https://example.com/evil-trout\"")
      end

      it "returns a list of topics if the top_period is not valid" do
        good_topic = Fabricate(:topic, like_count: 1000, posts_count: 100)
        TopTopic.refresh!
        TopicQuery.any_instance.expects(:list_top_for).never

        get "/embed/topics?discourse_embed_id=de-1234&top_period=decadely",
            headers: {
              "REFERER" => "https://example.com/evil-trout",
            }

        expect(response.status).to eq(200)
        expect(response.headers["X-Frame-Options"]).to be_nil
        expect(response.body).to match("data-embed-id=\"de-1234\"")
        expect(response.body).to match("data-topic-id=\"#{good_topic.id}\"")
        expect(response.body).to match("data-topic-id=\"#{topic.id}\"")
        expect(response.body).to match("data-referer=\"https://example.com/evil-trout\"")
      end

      it "wraps the list in a custom class" do
        get "/embed/topics?discourse_embed_id=de-1234&embed_class=my-special-class",
            headers: {
              "REFERER" => "https://example.com/evil-trout",
            }

        expect(response.status).to eq(200)
        expect(response.headers["X-Frame-Options"]).to be_nil
        expect(response.body).to match("class='topics-list my-special-class'")
      end

      it "returns no referer if not supplied" do
        get "/embed/topics?discourse_embed_id=de-1234"

        expect(response.status).to eq(200)
        expect(response.body).to match("data-referer=\"\"")
      end

      it "returns * for the referer if `embed_any_origin` is set" do
        SiteSetting.embed_any_origin = true

        get "/embed/topics?discourse_embed_id=de-1234"

        expect(response.status).to eq(200)
        expect(response.body).to match("data-referer=\"\\*\"")
      end

      it "disallows indexing the embed topic list" do
        get "/embed/topics?discourse_embed_id=de-1234",
            headers: {
              "REFERER" => "https://example.com/evil-trout",
            }

        expect(response.status).to eq(200)
        expect(response.headers["X-Robots-Tag"]).to match(/noindex/)
      end
    end
  end

  describe "#comments" do
    it "is 404 without an embed_url" do
      get "/embed/comments"

      expect(response.body).to match(I18n.t("embed.error"))
    end

    it "raises an error with a missing host" do
      get "/embed/comments", params: { embed_url: embed_url }

      expect(response.body).to match(I18n.t("embed.error"))
    end

    describe "by topic id" do
      fab!(:embeddable_host)

      it "allows a topic to be embedded by id" do
        get "/embed/comments",
            params: {
              topic_id: topic.id,
            },
            headers: {
              "REFERER" => "http://eviltrout.com/some-page",
            }

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

      it "does not share an anonymous cached response between referers" do
        global_setting :anon_cache_store_threshold, 1
        Middleware::AnonymousCache.enable_anon_cache
        Middleware::AnonymousCache.clear_all_cache!

        attacker_referer = "https://origin-a.example/page"
        victim_referer = "https://origin-b.example/page"

        get "/embed/comments",
            params: {
              topic_id: topic.id,
            },
            headers: {
              "REFERER" => attacker_referer,
            }

        expect(response.status).to eq(200)
        expect(response.headers["X-Discourse-Cached"]).to eq("store")
        expect(response.body).to include("data-referer=\"#{attacker_referer}\"")

        get "/embed/comments",
            params: {
              topic_id: topic.id,
            },
            headers: {
              "REFERER" => victim_referer,
            }

        expect(response.status).to eq(200)
        expect(response.headers["X-Discourse-Cached"]).to eq("store")
        expect(response.body).to include("data-referer=\"#{victim_referer}\"")
      end

      fab!(:attacker, :trust_level_1)
      fab!(:existing_post) { Fabricate(:post, topic: topic) }

      it "does not reinterpret an allowlisted non-Vimeo iframe as Vimeo content" do
        iframe_source = "https://www.instagram.com/?x=player.vimeo.com/<img src=x onerror=alert(1)>"
        sign_in(attacker)

        post "/posts.json",
             params: {
               raw: %(<iframe src="#{iframe_source}"></iframe>),
               topic_id: topic.id,
             }

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

        created_post = Post.find(response.parsed_body["id"])
        cooked_iframe = Nokogiri::HTML5.fragment(created_post.cooked).at_css("iframe")

        aggregate_failures do
          expect(created_post).to have_attributes(hidden: false, deleted_at: nil)
          expect(cooked_iframe["src"]).to eq(iframe_source)
        end

        sign_out
        get "/embed/comments", params: { topic_id: topic.id }

        document = Nokogiri.HTML5(response.body)
        rendered_iframe = document.at_css("article#post-#{created_post.id} .cooked > iframe")

        aggregate_failures do
          expect(response.status).to eq(200)
          expect(rendered_iframe).to be_present
          expect(rendered_iframe&.[]("src")).to eq(iframe_source)
          expect(document.css("img[onerror]")).to be_empty
        end
      end

      it "does not publish an iframe that traverses an allowlist path boundary" do
        iframe_source = "https://www.example.com/wild/preview/..\\outside"
        SiteSetting.allowed_iframes = "https://www.example.com/*/preview/"
        sign_in(attacker)

        post "/posts.json",
             params: {
               raw: %(<iframe src="#{iframe_source}"></iframe>),
               topic_id: topic.id,
             }

        created_post = Post.find(response.parsed_body["id"])

        aggregate_failures do
          expect(response.status).to eq(200)
          expect(response.parsed_body["id"]).to eq(created_post.id)
          expect(created_post.cooked).not_to include(iframe_source)
        end
      end
    end

    describe "full_app redirect" do
      fab!(:embeddable_host)

      before { SiteSetting.embed_full_app = true }

      it "redirects to topic URL with embed_mode when full_app is present" do
        topic_embed = Fabricate(:topic_embed, embed_url: embed_url)

        get "/embed/comments",
            params: {
              embed_url: embed_url,
              full_app: "true",
            },
            headers: {
              "REFERER" => embed_url,
            }

        expect(response).to redirect_to("#{topic_embed.topic.url}?embed_mode=true")
      end

      it "redirects to topic URL with embed_mode when using topic_id" do
        get "/embed/comments",
            params: {
              topic_id: topic.id,
              full_app: "true",
            },
            headers: {
              "REFERER" => "http://eviltrout.com/some-page",
            }

        expect(response).to redirect_to("#{topic.url}?embed_mode=true")
      end

      it "forwards class_name to the topic URL" do
        get "/embed/comments",
            params: {
              topic_id: topic.id,
              full_app: "true",
              class_name: "lee-af",
            },
            headers: {
              "REFERER" => "http://eviltrout.com/some-page",
            }

        expect(response).to redirect_to("#{topic.url}?class_name=lee-af&embed_mode=true")
      end

      it "redirects blank-slug topics to a slugless URL" do
        topic_embed = Fabricate(:topic_embed, embed_url: embed_url)
        topic_embed.topic.update_columns(title: "", slug: nil)
        topic_embed.topic.reload

        get "/embed/comments",
            params: {
              embed_url: embed_url,
              full_app: "true",
            },
            headers: {
              "REFERER" => embed_url,
            }

        expect(response).to redirect_to("#{topic_embed.topic.url}?embed_mode=true")
      end

      it "does not redirect when embed_full_app is disabled" do
        SiteSetting.embed_full_app = false
        topic_embed = Fabricate(:topic_embed, embed_url: embed_url)

        get "/embed/comments",
            params: {
              embed_url: embed_url,
              full_app: "true",
            },
            headers: {
              "REFERER" => embed_url,
            }

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

    context "with a host" do
      fab!(:embeddable_host)

      it "doesn't raise an error with no referer" do
        get "/embed/comments", params: { embed_url: embed_url }

        expect(response.body).not_to match(I18n.t("embed.error"))
      end

      it "includes CSS from embedded_scss field" do
        theme = Fabricate(:theme)
        theme.set_default!

        ThemeField.create!(
          theme_id: theme.id,
          name: "embedded_scss",
          target_id: 0,
          type_id: 1,
          value: ".test-osama-15 { color: red }",
        )

        topic_embed = Fabricate(:topic_embed, embed_url: embed_url)
        Fabricate(:post, topic: topic_embed.topic)

        get "/embed/comments", params: { embed_url: embed_url }, headers: { "REFERER" => embed_url }

        html = Nokogiri::HTML5.fragment(response.body)
        get html.at("link[data-target=embedded_theme]").attribute("href").value
        expect(response.status).to eq(200)
        expect(response.body).to include(".test-osama-15")
      end

      it "includes HTML from embedded_header field" do
        theme = Fabricate(:theme)
        theme.set_default!

        ThemeField.create!(
          theme_id: theme.id,
          name: "embedded_header",
          target_id: 0,
          type_id: 0,
          value: "<strong class='custom-text'>hey there!</strong>\n",
        )

        topic_embed = Fabricate(:topic_embed, embed_url: embed_url)
        post = Fabricate(:post, topic: topic_embed.topic)

        get "/embed/comments", params: { embed_url: embed_url }, headers: headers

        html = Nokogiri::HTML5.fragment(response.body)
        custom_header = html.at(".custom-text")

        expect(custom_header.name).to eq("strong")
        expect(custom_header.text).to eq("hey there!")
      end

      context "with success" do
        before { Jobs.run_immediately! }

        it "tells the topic retriever to work when no previous embed is found" do
          TopicRetriever.any_instance.expects(:retrieve)

          get "/embed/comments",
              params: {
                embed_url: embed_url,
              },
              headers: {
                "REFERER" => embed_url,
              }

          expect(response.status).to eq(200)
          expect(response.headers["X-Frame-Options"]).to be_nil
        end

        it "displays the right view" do
          topic_embed = Fabricate(:topic_embed, embed_url: embed_url)

          get "/embed/comments",
              params: {
                embed_url: embed_url_secure,
              },
              headers: {
                "REFERER" => embed_url,
              }

          expect(response.status).to eq(200)
          expect(response.headers["X-Frame-Options"]).to be_nil
          expect(response.body).to match(I18n.t("embed.start_discussion"))
        end

        it "creates a topic view when a topic_id is found" do
          topic_embed = Fabricate(:topic_embed, embed_url: embed_url)

          post = Fabricate(:post, topic: topic_embed.topic)

          get "/embed/comments",
              params: {
                embed_url: embed_url,
              },
              headers: {
                "REFERER" => embed_url,
              }

          expect(response.status).to eq(200)
          expect(response.headers["X-Frame-Options"]).to be_nil
          expect(response.body).to match(I18n.t("embed.continue"))
          expect(response.body).to match(post.cooked)
          expect(response.body).to match("<span class='replies'>1 reply</span>")

          small_action = Fabricate(:small_action, topic: topic_embed.topic)

          get "/embed/comments",
              params: {
                embed_url: embed_url,
              },
              headers: {
                "REFERER" => embed_url,
              }

          expect(response.status).to eq(200)
          expect(response.headers["X-Frame-Options"]).to be_nil
          expect(response.body).not_to match("post-#{small_action.id}")
          expect(response.body).to match("<span class='replies'>1 reply</span>")
        end

        it "does not forward user-supplied discourse_username to TopicRetriever" do
          captured_opts = nil
          original_new = TopicRetriever.method(:new)
          TopicRetriever
            .stubs(:new)
            .with do |url, opts|
              captured_opts = opts
              true
            end
            .returns(stub(retrieve: nil))

          get "/embed/comments",
              params: {
                embed_url: embed_url,
                discourse_username: "admin",
              },
              headers: {
                "REFERER" => embed_url,
              }

          expect(response.status).to eq(200)
          expect(captured_opts).to be_present
          expect(captured_opts[:author_username]).to be_nil
        end
      end
    end

    context "with multiple hosts" do
      fab!(:embeddable_host_1, :embeddable_host)
      fab!(:embeddable_host_2) { Fabricate(:embeddable_host, host: "http://discourse.org") }
      fab!(:embeddable_host_3) do
        Fabricate(:embeddable_host, host: "https://example.com/1234", class_name: "example")
      end

      context "with success" do
        it "works with the first host" do
          get "/embed/comments",
              params: {
                embed_url: embed_url,
              },
              headers: {
                "REFERER" => "http://eviltrout.com/wat/1-2-3.html",
              }

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

        it "works with the second host" do
          get "/embed/comments",
              params: {
                embed_url: embed_url,
              },
              headers: {
                "REFERER" => "https://discourse.org/blog-entry-1",
              }

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

        it "works with a host with a path" do
          get "/embed/comments",
              params: {
                embed_url: embed_url,
              },
              headers: {
                "REFERER" => "https://example.com/some-other-path",
              }

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

        it "contains custom class name" do
          get "/embed/comments",
              params: {
                embed_url: embed_url,
              },
              headers: {
                "REFERER" => "https://example.com/some-other-path",
              }

          expect(response.body).to match('class="example"')
        end

        it "contains custom class name from params" do
          get "/embed/comments",
              params: {
                embed_url: embed_url,
                class_name: "param-class-name",
              },
              headers: {
                "REFERER" => "https://example.com/some-other-path",
              }

          expect(response.body).to match('class="param-class-name"')
        end
      end

      context "with CSP frame-ancestors enabled" do
        before { SiteSetting.content_security_policy_frame_ancestors = true }

        it "includes all the hosts" do
          get "/embed/comments",
              params: {
                embed_url: embed_url,
              },
              headers: {
                "REFERER" => "http://eviltrout.com/wat/1-2-3.html",
              }

          expect(response.headers["Content-Security-Policy"]).to match(
            %r{frame-ancestors.*https://discourse\.org},
          )
          expect(response.headers["Content-Security-Policy"]).to match(
            %r{frame-ancestors.*https://example\.com},
          )
        end
      end
    end
  end

  describe "#count" do
    fab!(:embeddable_host)

    it "returns counts for public topics" do
      topic_embed = Fabricate(:topic_embed, embed_url: "http://eviltrout.com/public-article")

      get "/embed/count.json", params: { embed_url: ["http://eviltrout.com/public-article"] }

      expect(response.status).to eq(200)
      expect(response.parsed_body["counts"]).to have_key("http://eviltrout.com/public-article")
    end

    it "does not return counts for topics in restricted categories" do
      restricted_category = Fabricate(:category)
      restricted_category.set_permissions(staff: :full)
      restricted_category.save!

      restricted_topic = Fabricate(:topic, category: restricted_category, posts_count: 5)
      Fabricate(
        :topic_embed,
        post: Fabricate(:post, topic: restricted_topic),
        topic: restricted_topic,
        embed_url: "http://eviltrout.com/private-article",
      )

      get "/embed/count.json", params: { embed_url: ["http://eviltrout.com/private-article"] }

      expect(response.status).to eq(200)
      expect(response.parsed_body["counts"]).not_to have_key("http://eviltrout.com/private-article")
    end

    it "returns counts only for visible topics when both public and restricted are requested" do
      public_topic = Fabricate(:topic, posts_count: 3)
      Fabricate(
        :topic_embed,
        post: Fabricate(:post, topic: public_topic),
        topic: public_topic,
        embed_url: "http://eviltrout.com/public-post",
      )

      restricted_category = Fabricate(:category)
      restricted_category.set_permissions(staff: :full)
      restricted_category.save!

      restricted_topic = Fabricate(:topic, category: restricted_category, posts_count: 5)
      Fabricate(
        :topic_embed,
        post: Fabricate(:post, topic: restricted_topic),
        topic: restricted_topic,
        embed_url: "http://eviltrout.com/secret-post",
      )

      get "/embed/count.json",
          params: {
            embed_url: %w[http://eviltrout.com/public-post http://eviltrout.com/secret-post],
          }

      expect(response.status).to eq(200)
      counts = response.parsed_body["counts"]
      expect(counts).to have_key("http://eviltrout.com/public-post")
      expect(counts).not_to have_key("http://eviltrout.com/secret-post")
    end
  end
end
