# frozen_string_literal: true

require "abstract_unit"
require "timeout"
require "rack/content_length"

class ResponseTest < ActiveSupport::TestCase
  def setup
    @response = ActionDispatch::Response.create
    @response.request = ActionDispatch::Request.empty
  end

  def test_can_wait_until_commit
    t = Thread.new {
      @response.await_commit
    }
    @response.commit!
    assert_predicate @response, :committed?
    assert t.join(0.5)
  end

  def test_stream_close
    @response.stream.close
    assert_predicate @response.stream, :closed?
  end

  def test_stream_write
    @response.stream.write "foo"
    @response.stream.close
    assert_equal "foo", @response.body
  end

  def test_write_after_close
    @response.stream.close

    e = assert_raises(IOError) do
      @response.stream.write "omg"
    end
    assert_equal "closed stream", e.message
  end

  def test_each_isnt_called_if_str_body_is_written
    # Controller writes and reads response body
    each_counter = 0
    @response.body = Object.new.tap { |o| o.singleton_class.define_method(:each) { |&block| each_counter += 1; block.call "foo" } }
    @response["X-Foo"] = @response.body

    assert_equal 1, each_counter, "#each was not called once"

    # Build response
    status, headers, body = @response.to_a

    assert_equal 200, status
    assert_equal "foo", headers["X-Foo"]
    assert_equal "foo", body.each.to_a.join

    # Show that #each was not called twice
    assert_equal 1, each_counter, "#each was not called once"
  end

  def test_set_header_after_read_body_during_action
    @response.body

    # set header after the action reads back @response.body
    @response["x-header"] = "Best of all possible worlds."

    # the response can be built.
    status, headers, body = @response.to_a
    assert_equal 200, status
    assert_equal "", body.body

    assert_equal "Best of all possible worlds.", headers["x-header"]
  end

  def test_read_body_during_action
    @response.body = "Hello, World!"

    # even though there's no explicitly set content-type,
    assert_nil @response.content_type

    # after the action reads back @response.body,
    assert_equal "Hello, World!", @response.body

    # the response can be built.
    status, headers, body = @response.to_a
    assert_equal 200, status
    assert_equal({
      "Content-Type" => "text/html; charset=utf-8"
    }, headers)

    parts = []
    body.each { |part| parts << part }
    assert_equal ["Hello, World!"], parts
  end

  def test_response_body_encoding
    body = ["hello".encode(Encoding::UTF_8)]
    response = ActionDispatch::Response.new 200, {}, body
    response.request = ActionDispatch::Request.empty
    assert_equal Encoding::UTF_8, response.body.encoding
  end

  def test_response_charset_writer
    @response.charset = "utf-16"
    assert_equal "utf-16", @response.charset
    @response.charset = nil
    assert_equal "utf-8", @response.charset
  end

  def test_setting_content_type_header_impacts_content_type_method
    @response.headers["Content-Type"] = "application/aaron"
    assert_equal "application/aaron", @response.content_type
  end

  def test_empty_content_type_returns_nil
    @response.headers["Content-Type"] = ""
    assert_nil @response.content_type
  end

  test "simple output" do
    @response.body = "Hello, World!"

    status, headers, body = @response.to_a
    assert_equal 200, status
    assert_equal({
      "Content-Type" => "text/html; charset=utf-8"
    }, headers)

    parts = []
    body.each { |part| parts << part }
    assert_equal ["Hello, World!"], parts
  end

  test "status handled properly in initialize" do
    assert_equal 200, ActionDispatch::Response.new("200 OK").status
  end

  def test_only_set_charset_still_defaults_to_text_html
    response = ActionDispatch::Response.new
    response.charset = "utf-16"
    _, headers, _ = response.to_a
    assert_equal "text/html; charset=utf-16", headers["Content-Type"]
  end

  test "utf8 output" do
    @response.body = [1090, 1077, 1089, 1090].pack("U*")

    status, headers, _ = @response.to_a
    assert_equal 200, status
    assert_equal({
      "Content-Type" => "text/html; charset=utf-8"
    }, headers)
  end

  test "content length" do
    [100, 101, 102, 204].each do |c|
      @response = ActionDispatch::Response.new
      @response.status = c.to_s
      @response.set_header "Content-Length", "0"
      _, headers, _ = @response.to_a
      assert_not headers.has_key?("Content-Length"), "#{c} must not have a Content-Length header field"
    end
  end

  test "does not contain a message-body" do
    [100, 101, 102, 204, 304].each do |c|
      @response = ActionDispatch::Response.new
      @response.status = c.to_s
      @response.body = "Body must not be included"
      _, _, body = @response.to_a
      assert_empty body, "#{c} must not have a message-body but actually contains #{body}"
    end
  end

  test "content type" do
    [204, 304].each do |c|
      @response = ActionDispatch::Response.new
      @response.status = c.to_s
      _, headers, _ = @response.to_a
      assert_not headers.has_key?("Content-Type"), "#{c} should not have Content-Type header"
    end

    [200, 302, 404, 500].each do |c|
      @response = ActionDispatch::Response.new
      @response.status = c.to_s
      _, headers, _ = @response.to_a
      assert headers.has_key?("Content-Type"), "#{c} did not have Content-Type header"
    end
  end

  test "does not include Status header" do
    @response.status = "200 OK"
    _, headers, _ = @response.to_a
    assert_not headers.has_key?("Status")
  end

  test "response code" do
    @response.status = "200 OK"
    assert_equal 200, @response.response_code

    @response.status = "200"
    assert_equal 200, @response.response_code

    @response.status = 200
    assert_equal 200, @response.response_code
  end

  test "code" do
    @response.status = "200 OK"
    assert_equal "200", @response.code

    @response.status = "200"
    assert_equal "200", @response.code

    @response.status = 200
    assert_equal "200", @response.code
  end

  test "message" do
    @response.status = "200 OK"
    assert_equal "OK", @response.message

    @response.status = "200"
    assert_equal "OK", @response.message

    @response.status = 200
    assert_equal "OK", @response.message
  end

  test "cookies" do
    @response.set_cookie("user_name", value: "david", path: "/")
    _status, headers, _body = @response.to_a
    assert_equal "user_name=david; path=/", headers["Set-Cookie"]
    assert_equal({ "user_name" => "david" }, @response.cookies)
  end

  test "multiple cookies" do
    @response.set_cookie("user_name", value: "david", path: "/")
    @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10, 5))
    _status, headers, _body = @response.to_a
    assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000", headers["Set-Cookie"]
    assert_equal({ "login" => "foo&bar", "user_name" => "david" }, @response.cookies)
  end

  test "delete cookies" do
    @response.set_cookie("user_name", value: "david", path: "/")
    @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10, 5))
    @response.delete_cookie("login")
    assert_equal({ "user_name" => "david", "login" => nil }, @response.cookies)
  end

  test "read ETag and Cache-Control" do
    resp = ActionDispatch::Response.new.tap { |response|
      response.cache_control[:public] = true
      response.etag = "123"
      response.body = "Hello"
    }
    resp.to_a

    assert_predicate resp, :etag?
    assert_predicate resp, :weak_etag?
    assert_not_predicate resp, :strong_etag?
    assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag)
    assert_equal({ public: true }, resp.cache_control)

    assert_equal("public", resp.headers["Cache-Control"])
    assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.headers["ETag"])
  end

  test "read strong ETag" do
    resp = ActionDispatch::Response.new.tap { |response|
      response.cache_control[:public] = true
      response.strong_etag = "123"
      response.body = "Hello"
    }
    resp.to_a

    assert_predicate resp, :etag?
    assert_not_predicate resp, :weak_etag?
    assert_predicate resp, :strong_etag?
    assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag)
  end

  test "read charset and content type" do
    resp = ActionDispatch::Response.new.tap { |response|
      response.charset = "utf-16"
      response.content_type = Mime[:xml]
      response.body = "Hello"
    }
    resp.to_a

    assert_equal("utf-16", resp.charset)
    assert_equal(Mime[:xml], resp.media_type)
    assert_equal("application/xml; charset=utf-16", resp.content_type)
    assert_equal("application/xml; charset=utf-16", resp.headers["Content-Type"])
  end

  test "read content type with default charset utf-8" do
    resp = ActionDispatch::Response.new(200, "Content-Type" => "text/xml")
    assert_equal("utf-8", resp.charset)
  end

  test "read content type with charset utf-16" do
    original = ActionDispatch::Response.default_charset
    begin
      ActionDispatch::Response.default_charset = "utf-16"
      resp = ActionDispatch::Response.new(200, "Content-Type" => "text/xml")
      assert_equal("utf-16", resp.charset)
    ensure
      ActionDispatch::Response.default_charset = original
    end
  end

  test "read x_frame_options, x_content_type_options, x_xss_protection, x_download_options and x_permitted_cross_domain_policies, referrer_policy" do
    original_default_headers = ActionDispatch::Response.default_headers
    begin
      ActionDispatch::Response.default_headers = {
        "X-Frame-Options" => "DENY",
        "X-Content-Type-Options" => "nosniff",
        "X-XSS-Protection" => "1;",
        "X-Download-Options" => "noopen",
        "X-Permitted-Cross-Domain-Policies" => "none",
        "Referrer-Policy" => "strict-origin-when-cross-origin"
      }
      resp = ActionDispatch::Response.create.tap { |response|
        response.body = "Hello"
      }
      resp.to_a

      assert_equal("DENY", resp.headers["X-Frame-Options"])
      assert_equal("nosniff", resp.headers["X-Content-Type-Options"])
      assert_equal("1;", resp.headers["X-XSS-Protection"])
      assert_equal("noopen", resp.headers["X-Download-Options"])
      assert_equal("none", resp.headers["X-Permitted-Cross-Domain-Policies"])
      assert_equal("strict-origin-when-cross-origin", resp.headers["Referrer-Policy"])
    ensure
      ActionDispatch::Response.default_headers = original_default_headers
    end
  end

  test "read custom default_header" do
    original_default_headers = ActionDispatch::Response.default_headers
    begin
      ActionDispatch::Response.default_headers = {
        "X-XX-XXXX" => "Here is my phone number"
      }
      resp = ActionDispatch::Response.create.tap { |response|
        response.body = "Hello"
      }
      resp.to_a

      assert_equal("Here is my phone number", resp.headers["X-XX-XXXX"])
    ensure
      ActionDispatch::Response.default_headers = original_default_headers
    end
  end

  test "respond_to? accepts include_private" do
    assert_not_respond_to @response, :method_missing
    assert @response.respond_to?(:method_missing, true)
  end

  test "can be explicitly destructured into status, headers and an enumerable body" do
    response = ActionDispatch::Response.new(404, { "Content-Type" => "text/plain" }, ["Not Found"])
    response.request = ActionDispatch::Request.empty
    status, headers, body = *response

    assert_equal 404, status
    assert_equal({ "Content-Type" => "text/plain" }, headers)
    assert_equal ["Not Found"], body.each.to_a
  end

  test "[response.to_a].flatten does not recurse infinitely" do
    Timeout.timeout(1) do # use a timeout to prevent it stalling indefinitely
      status, headers, body = [@response.to_a].flatten
      assert_equal @response.status, status
      assert_equal @response.headers, headers
      assert_equal @response.body, body.each.to_a.join
    end
  end

  test "compatibility with Rack::ContentLength" do
    @response.body = "Hello"
    app = lambda { |env| @response.to_a }
    env = Rack::MockRequest.env_for("/")

    _status, headers, _body = app.call(env)
    assert_nil headers["Content-Length"]

    _status, headers, _body = Rack::ContentLength.new(app).call(env)
    assert_equal "5", headers["Content-Length"]
  end
end

class ResponseHeadersTest < ActiveSupport::TestCase
  def setup
    @response = ActionDispatch::Response.create
    @response.set_header "Foo", "1"
  end

  test "has_header?" do
    assert @response.has_header? "Foo"
    assert_not @response.has_header? "foo"
    assert_not @response.has_header? nil
  end

  test "get_header" do
    assert_equal "1", @response.get_header("Foo")
    assert_nil @response.get_header("foo")
    assert_nil @response.get_header(nil)
  end

  test "set_header" do
    assert_equal "2", @response.set_header("Foo", "2")
    assert @response.has_header?("Foo")
    assert_equal "2", @response.get_header("Foo")

    assert_nil @response.set_header("Foo", nil)
    assert @response.has_header?("Foo")
    assert_nil @response.get_header("Foo")
  end

  test "delete_header" do
    assert_nil @response.delete_header(nil)

    assert_nil @response.delete_header("foo")
    assert @response.has_header?("Foo")

    assert_equal "1", @response.delete_header("Foo")
    assert_not @response.has_header?("Foo")
  end

  test "add_header" do
    # Add a value to an existing header
    assert_equal "1,2", @response.add_header("Foo", "2")
    assert_equal "1,2", @response.get_header("Foo")

    # Add nil to an existing header
    assert_equal "1,2", @response.add_header("Foo", nil)
    assert_equal "1,2", @response.get_header("Foo")

    # Add nil to a nonexistent header
    assert_nil @response.add_header("Bar", nil)
    assert_not @response.has_header?("Bar")
    assert_nil @response.get_header("Bar")

    # Add a value to a nonexistent header
    assert_equal "1", @response.add_header("Bar", "1")
    assert @response.has_header?("Bar")
    assert_equal "1", @response.get_header("Bar")
  end
end

class ResponseIntegrationTest < ActionDispatch::IntegrationTest
  test "response cache control from railsish app" do
    @app = lambda { |env|
      ActionDispatch::Response.new.tap { |resp|
        resp.cache_control[:public] = true
        resp.etag = "123"
        resp.body = "Hello"
        resp.request = ActionDispatch::Request.empty
      }.to_a
    }

    get "/"
    assert_response :success

    assert_equal("public", @response.headers["Cache-Control"])
    assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])

    assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag)
    assert_equal({ public: true }, @response.cache_control)
  end

  test "response cache control from rackish app" do
    @app = lambda { |env|
      [200,
        { "ETag" => 'W/"202cb962ac59075b964b07152d234b70"',
          "Cache-Control" => "public" }, ["Hello"]]
    }

    get "/"
    assert_response :success

    assert_equal("public", @response.headers["Cache-Control"])
    assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])

    assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag)
    assert_equal({ public: true }, @response.cache_control)
  end

  test "response charset and content type from railsish app" do
    @app = lambda { |env|
      ActionDispatch::Response.new.tap { |resp|
        resp.charset = "utf-16"
        resp.content_type = Mime[:xml]
        resp.body = "Hello"
        resp.request = ActionDispatch::Request.empty
      }.to_a
    }

    get "/"
    assert_response :success

    assert_equal("utf-16", @response.charset)
    assert_equal(Mime[:xml], @response.media_type)
    assert_equal("application/xml; charset=utf-16", @response.content_type)
    assert_equal("application/xml; charset=utf-16", @response.headers["Content-Type"])
  end

  test "response charset and content type from rackish app" do
    @app = lambda { |env|
      [200,
        { "Content-Type" => "application/xml; charset=utf-16" },
        ["Hello"]]
    }

    get "/"
    assert_response :success

    assert_equal("utf-16", @response.charset)
    assert_equal(Mime[:xml], @response.media_type)
    assert_equal("application/xml; charset=utf-16", @response.content_type)
    assert_equal("application/xml; charset=utf-16", @response.headers["Content-Type"])
  end

  test "strong ETag validator" do
    @app = lambda { |env|
      ActionDispatch::Response.new.tap { |resp|
        resp.strong_etag = "123"
        resp.body = "Hello"
        resp.request = ActionDispatch::Request.empty
      }.to_a
    }

    get "/"
    assert_response :ok

    assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])
    assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag)
  end

  test "response Content-Type with optional parameters" do
    @app = lambda { |env|
      [
        200,
        { "Content-Type" => "text/csv; charset=utf-16; header=present" },
        ["Hello"]
      ]
    }

    get "/"
    assert_response :success

    assert_equal("text/csv; charset=utf-16; header=present", @response.headers["Content-Type"])
    assert_equal("text/csv; charset=utf-16; header=present", @response.content_type)
    assert_equal("text/csv", @response.media_type)
    assert_equal("utf-16", @response.charset)
  end

  test "response Content-Type with optional parameters that set before charset" do
    @app = lambda { |env|
      [
        200,
        { "Content-Type" => "text/csv; header=present; charset=utf-16" },
        ["Hello"]
      ]
    }

    get "/"
    assert_response :success

    assert_equal("text/csv; header=present; charset=utf-16", @response.headers["Content-Type"])
    assert_equal("text/csv; header=present; charset=utf-16", @response.content_type)
    assert_equal("text/csv; header=present", @response.media_type)
    assert_equal("utf-16", @response.charset)
  end

  test "response Content-Type with quoted-string" do
    @app = lambda { |env|
      [
        200,
        { "Content-Type" => 'text/csv; header=present; charset="utf-16"' },
        ["Hello"]
      ]
    }

    get "/"
    assert_response :success

    assert_equal('text/csv; header=present; charset="utf-16"', @response.headers["Content-Type"])
    assert_equal('text/csv; header=present; charset="utf-16"', @response.content_type)
    assert_equal("text/csv; header=present", @response.media_type)
    assert_equal("utf-16", @response.charset)
  end

  test "`content type` returns header that excludes `charset` when specified `return_only_media_type_on_content_type`" do
    original = ActionDispatch::Response.return_only_media_type_on_content_type
    ActionDispatch::Response.return_only_media_type_on_content_type = true

    @app = lambda { |env|
      if env["PATH_INFO"] == "/with_parameters"
        [200, { "Content-Type" => "text/csv; header=present; charset=utf-16" }, [""]]
      else
        [200, { "Content-Type" => "text/csv; charset=utf-16" }, [""]]
      end
    }

    get "/"
    assert_response :success

    assert_deprecated do
      assert_equal("text/csv", @response.content_type)
    end

    get "/with_parameters"
    assert_response :success

    assert_deprecated do
      assert_equal("text/csv; header=present", @response.content_type)
    end
  ensure
    ActionDispatch::Response.return_only_media_type_on_content_type = original
  end
end
