require "../spec_helper"
require "../../app/models/*"

API_KEY = Random::Secure.urlsafe_base64

describe "App::Controllers::Link" do
  describe "Create" do
    it "should create link" do
      test_user = create_test_user()

      payload = {"url" => "https://kagi.com"}
      post(
        "/api/links",
        headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
        body: payload.to_json
      )

      parsed_response = Hash(String, Hash(String, String | Int64 | Array(Hash(String, String)))).from_json(response.body)
      parsed_response["data"]["origin"].should eq(payload["url"])
    end

    it "should return existing link if url already exists" do
      test_user = create_test_user()

      payload = {"url" => "http://idonthavespotify.donado.co"}
      post(
        "/api/links",
        headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
        body: payload.to_json
      )

      first_response = Hash(String, Hash(String, String | Int64 | Array(Hash(String, String)))).from_json(response.body)
      first_response["data"]["origin"].should eq(payload["url"])

      post(
        "/api/links",
        headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
        body: payload.to_json
      )

      second_response = Hash(String, Hash(String, String | Int64 | Array(Hash(String, String)))).from_json(response.body)
      second_response["data"]["origin"].should eq(payload["url"])
      second_response["data"]["id"].should eq(first_response["data"]["id"])
    end

    it "should return 400 - url required field" do
      test_user = create_test_user()

      payload = {"test" => "https://kagi.com"}
      post(
        "/api/links",
        headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
        body: payload.to_json
      )

      expected = {"error" => "url: Required field"}.to_json
      response.body.should eq(expected)
    end

    it "should return 400 - invalid url" do
      test_user = create_test_user()

      payload = {"url" => "test"}
      post(
        "/api/links",
        headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
        body: payload.to_json
      )

      expected = {"errors" => {"url" => ["is invalid"]}}.to_json
      response.body.should eq(expected)
    end

    it "should return 401 - missing api key" do
      payload = {"url" => "https://kagi.com"}
      post("/api/links", headers: HTTP::Headers{"Content-Type" => "application/json"}, body: payload.to_json)

      expected = {"error" => "Unauthorized access"}.to_json
      response.status_code.should eq(401)
      response.body.should eq(expected)
    end
  end

  describe "Index" do
    it "should redirect to origin domain with forwarded headers" do
      link = "https://test.com"
      test_user = create_test_user()

      test_link = create_test_link(test_user, link)
      serialized_link = App::Serializers::Link.new(test_link)

      user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0"

      get(serialized_link.refer, headers: HTTP::Headers{
        "X-Api-Key" => test_user.api_key.to_s,
        "User-Agent" => user_agent
      })

      response.headers["Location"].should eq(link)
      response.headers["User-Agent"].should eq(user_agent)
      response.headers.has_key?("X-Forwarded-For").should be_true
    end

    it "should create a new click after redirect with proper information" do
      link = "https://sjdonado.com"
      test_user = create_test_user()

      test_link = create_test_link(test_user, link)
      serialized_link = App::Serializers::Link.new(test_link)

      user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0"
      referer = "https://example.com/page"

      get(serialized_link.refer, headers: HTTP::Headers{
        "User-Agent" => user_agent,
        "Referer" => referer
      })

      Fiber.yield # replace yield with sleep 5 to debug errors

      response.headers["Location"].should eq(link)

      # Verify that the click was recorded
      updated_test_link = get_test_link(test_link.id)
      updated_test_link.clicks.size.should eq(test_link.clicks.size + 1)

      # Verify click details
      latest_click = updated_test_link.clicks.last
      latest_click.user_agent.should eq(user_agent)
      latest_click.browser.should eq("Firefox")
      latest_click.os.should eq("Mac OS X")
      latest_click.referer.should eq("example.com") # Should extract host from the referer
    end

    it "should create a click with utm_source when no referer is provided" do
      link = "https://sjdonado.com"
      test_user = create_test_user()

      test_link = create_test_link(test_user, link)
      serialized_link = App::Serializers::Link.new(test_link)

      # Add utm_source parameter
      get("#{serialized_link.refer}?utm_source=email_campaign")

      Fiber.yield

      updated_test_link = get_test_link(test_link.id)
      latest_click = updated_test_link.clicks.last
      latest_click.referer.should eq("email_campaign")
    end

    it "should return 404 - link does not exist" do
      test_user = create_test_user()

      get("https://localhost:4001/R4kj2")

      expected = {"error" => "Resource not found"}.to_json
      response.status_code.should eq(404)
      response.body.should eq(expected)
    end
  end

  describe "All" do
    it "should return all links with pagination" do
      links = ["https://sjdonado.com", "sjdonado.com", "sjdonado.com.co"]
      test_user = create_test_user()

      links.each do |link|
        create_test_link(test_user, link)
      end

      get("/api/links", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      parsed_response = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body)

      # Check that each link is in the response data
      origins = parsed_response["data"].as(Array).map { |link| link["origin"] }
      links.each do |link|
        origins.should contain(link)
      end

      parsed_response["pagination"].as(Hash)["has_more"].should be_false
    end

    it "should respect custom limit parameter" do
      test_user = create_test_user()

      5.times do |i|
        create_test_link(test_user, "https://example.com/#{i}")
      end

      get("/api/links?limit=2", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      parsed_response = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body)
      parsed_response["data"].as(Array).size.should eq(2)
      parsed_response["pagination"].as(Hash)["has_more"].should be_true
      parsed_response["pagination"].as(Hash)["next"].should_not be_nil
    end

    it "should support cursor-based pagination" do
      test_user = create_test_user()

      5.times do |i|
        create_test_link(test_user, "https://example.com/#{i}")
      end

      # Get first page
      get("/api/links?limit=2", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})
      first_page = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body)
      cursor = first_page["pagination"].as(Hash)["next"]

      # Get second page using cursor
      get("/api/links?limit=2&cursor=#{cursor}", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})
      second_page = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body)

      # Ensure different links are returned
      first_page_ids = first_page["data"].as(Array).map { |link| link["id"] }
      second_page_ids = second_page["data"].as(Array).map { |link| link["id"] }

      # Check that no IDs from first page appear in second page
      (first_page_ids & second_page_ids).empty?.should be_true
    end

    it "should return owned links only" do
      links = ["https://donado.co", "donado.co", "uninorte.edu.co", "kagi.com"]
      test_user = create_test_user()

      links[0..2].each do |link|
        create_test_link(test_user, link)
      end

      test_other_user = create_test_user()
      create_test_link(test_other_user, links[3])

      get("/api/links", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      parsed_response = Hash(String, Array(Hash(String, String | Int64)) | Hash(String, Bool | String?)).from_json(response.body)
      parsed_response["data"].as(Array).size.should eq(3)

      origins = parsed_response["data"].as(Array).map { |link| link["origin"] }
      links[0..2].each do |link|
        origins.should contain(link)
      end
      origins.should_not contain(links[3])
    end

    it "should return 401 - missing api key" do
      get "/api/links"

      expected = {"error" => "Unauthorized access"}.to_json
      response.status_code.should eq(401)
      response.body.should eq(expected)
    end
  end

  describe "Get" do
    it "should return the specified link with limited click details" do
      link = "https://bing.com"
      test_user = create_test_user()
      test_link = create_test_link(test_user, link)

      110.times do
        create_test_click(test_link)
      end

      get("/api/links/#{test_link.id}", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      parsed_response = Hash(String, Hash(String, String | Int64 | Array(Hash(String, String)))).from_json(response.body)
      parsed_response["data"]["origin"].should eq(link)
      parsed_response["data"]["clicks"].as(Array).size.should eq(100)
    end

    it "should return 404 - link does not exist" do
      test_user = create_test_user()

      get("/api/links/1", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      expected = {"error" => "Resource not found"}.to_json
      response.status_code.should eq(404)
      response.body.should eq(expected)
    end

    it "should return 401 - missing api key" do
      get "/api/links/1"

      expected = {"error" => "Unauthorized access"}.to_json
      response.status_code.should eq(401)
      response.body.should eq(expected)
    end
  end

  describe "Clicks" do
    it "should return paginated clicks for a link" do
      link = "https://example.com"
      test_user = create_test_user()
      test_link = create_test_link(test_user, link)

      5.times do
        create_test_click(test_link)
      end

      get("/api/links/#{test_link.id}/clicks", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      parsed_response = Hash(String, Array(Hash(String, String)) | Hash(String, Bool | String?)).from_json(response.body)
      parsed_response["data"].as(Array).size.should eq(5)
      parsed_response["pagination"].as(Hash)["has_more"].should be_false
    end

    it "should respect limit parameter" do
      link = "https://example.com"
      test_user = create_test_user()
      test_link = create_test_link(test_user, link)

      10.times do
        create_test_click(test_link)
      end

      get("/api/links/#{test_link.id}/clicks?limit=3", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      parsed_response = Hash(String, Array(Hash(String, String)) | Hash(String, Bool | String?)).from_json(response.body)
      parsed_response["data"].as(Array).size.should eq(3)
      parsed_response["pagination"].as(Hash)["has_more"].should be_true
      parsed_response["pagination"].as(Hash)["next"].should_not be_nil
    end

    it "should support cursor-based pagination" do
      link = "https://example.com"
      test_user = create_test_user()
      test_link = create_test_link(test_user, link)

      10.times do
        create_test_click(test_link)
      end

      # Get first page
      get("/api/links/#{test_link.id}/clicks?limit=3", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})
      first_page = Hash(String, Array(Hash(String, String)) | Hash(String, Bool | String?)).from_json(response.body)
      cursor = first_page["pagination"].as(Hash)["next"]

      # Get second page using cursor
      get("/api/links/#{test_link.id}/clicks?limit=3&cursor=#{cursor}", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})
      second_page = Hash(String, Array(Hash(String, String)) | Hash(String, Bool | String?)).from_json(response.body)

      # Ensure different clicks are returned
      first_page_ids = first_page["data"].as(Array).map { |click| click["id"] }
      second_page_ids = second_page["data"].as(Array).map { |click| click["id"] }

      # Check that no IDs from first page appear in second page
      (first_page_ids & second_page_ids).empty?.should be_true
    end

    it "should return 404 - link does not exist" do
      test_user = create_test_user()

      get("/api/links/nonexistent_id/clicks", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      expected = {"error" => "Resource not found"}.to_json
      response.status_code.should eq(404)
      response.body.should eq(expected)
    end

    it "should return 401 - missing api key" do
      get("/api/links/1/clicks")

      expected = {"error" => "Unauthorized access"}.to_json
      response.status_code.should eq(401)
      response.body.should eq(expected)
    end
  end

  describe "Update" do
    it "should update link url" do
      link = "https://github.com"
      test_user = create_test_user()
      test_link = create_test_link(test_user, link)

      payload = {"url" => "https://github.com.co"}
      put(
        "/api/links/#{test_link.id}",
        headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
        body: payload.to_json
      )

      parsed_response = Hash(String, Hash(String, String | Int64 | Array(Hash(String, String)))).from_json(response.body)
      parsed_response["data"]["origin"].should eq(payload["url"])
    end

    it "should return 404 - link does not exist" do
      test_user = create_test_user()

      payload = {"url" => "https://kagi.com.co"}
      put(
        "/api/links/1",
        headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
        body: payload.to_json
      )

      expected = {"error" => "Resource not found"}.to_json
      response.status_code.should eq(404)
      response.body.should eq(expected)
    end

    it "should return 401 - missing api key" do
      payload = {"url" => "https://kagi.com.co"}
      put(
        "/api/links/1",
        headers: HTTP::Headers{"Content-Type" => "application/json"},
        body: payload.to_json
      )

      expected = {"error" => "Unauthorized access"}.to_json
      response.status_code.should eq(401)
      response.body.should eq(expected)
    end
  end

  describe "Delete" do
    it "should delete link url" do
      link = "https://news.ycombinator.com"
      test_user = create_test_user()
      test_link = create_test_link(test_user, link)

      delete("/api/links/#{test_link.id}", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      response.status_code.should eq(204)
    end

    it "should return 404 - link does not exist" do
      test_user = create_test_user()

      delete("/api/links/1", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})

      expected = {"error" => "Resource not found"}.to_json
      response.status_code.should eq(404)
      response.body.should eq(expected)
    end

    it "should return 401 - missing api key" do
      delete "/api/links/1"

      expected = {"error" => "Unauthorized access"}.to_json
      response.status_code.should eq(401)
      response.body.should eq(expected)
    end
  end
end
