import _ from 'lodash'
import { addDefaultPort, parse as parseUrl } from './uri'
import debugModule from 'debug'
import { parseDomain } from './parse-domain'
import type { ParsedHost, ParsedHostWithProtocolAndHost } from './types'

export type Policy = 'same-origin' | 'same-super-domain-origin' | 'schemeful-same-site'

const debug = debugModule('cypress:network:cors')

export function getSuperDomain (url: string) {
  const parsed = parseUrlIntoHostProtocolDomainTldPort(url)

  return _.compact([parsed.domain, parsed.tld]).join('.')
}

export function parseUrlIntoHostProtocolDomainTldPort (str: string) {
  let { hostname, port, protocol } = parseUrl(str)

  if (!hostname) {
    hostname = ''
  }

  if (!port) {
    port = protocol === 'https:' ? '443' : '80'
  }

  let parsed: Partial<ParsedHostWithProtocolAndHost> | null = parseDomain(hostname)

  // if we couldn't get a parsed domain
  if (!parsed) {
    // then just fall back to a dumb check
    // based on assumptions that the tld
    // is the last segment after the final
    // '.' and that the domain is the segment
    // before that
    const segments = hostname.split('.')

    parsed = {
      subdomain: segments[segments.length - 3] || '',
      tld: segments[segments.length - 1] || '',
      domain: segments[segments.length - 2] || '',
    }
  }

  const obj: ParsedHostWithProtocolAndHost = {
    port,
    protocol,
    subdomain: parsed.subdomain || null,
    domain: parsed.domain,
    tld: parsed.tld,
  }

  debug('Parsed URL %o', obj)

  return obj
}

export function getDomainNameFromUrl (url: string) {
  const parsedHost = parseUrlIntoHostProtocolDomainTldPort(url)

  return getDomainNameFromParsedHost(parsedHost)
}

export function getDomainNameFromParsedHost (parsedHost: ParsedHost) {
  return _.compact([parsedHost.domain, parsedHost.tld]).join('.')
}

export function domainPropsToHostname ({ domain, subdomain, tld }: Record<string, any>) {
  return _.compact([subdomain, domain, tld]).join('.')
}

/**
 * same-origin: Whether or not a urls scheme, port, and host match. @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
 * same-super-domain-origin: Whether or not a url's scheme, domain, top-level domain, and port match
 * same-site: Whether or not a url's scheme, domain, and top-level domain match. @see https://developer.mozilla.org/en-US/docs/Glossary/Site
 * @param {Policy} policy - the policy being used
 * @param {string} frameUrl - the url being compared
 * @param {ParsedHostWithProtocolAndHost} topProps - the props being compared against the url
 * @returns {boolean} whether or not the props and url fit the policy
 */
export function urlMatchesPolicyProps ({ policy, frameUrl, topProps }: {
  policy: Policy
  frameUrl: string
  topProps: ParsedHostWithProtocolAndHost | null
}): boolean {
  if (!policy || !frameUrl || !topProps) {
    return false
  }

  const urlProps = parseUrlIntoHostProtocolDomainTldPort(frameUrl)

  switch (policy) {
    case 'same-origin': {
      // if same origin, all parts of the props needs to match, including subdomain and scheme
      return _.isEqual(urlProps, topProps)
    }
    case 'same-super-domain-origin':
    case 'schemeful-same-site': {
      const { port: port1, subdomain: _unused1, ...parsedUrl } = urlProps
      const { port: port2, subdomain: _unused2, ...relevantProps } = topProps

      let doPortsPassSameSchemeCheck: boolean

      if (policy === 'same-super-domain-origin') {
        // if a super domain origin comparison, the ports MUST be strictly equal
        doPortsPassSameSchemeCheck = port1 === port2
      } else {
        // otherwise, this is a same-site comparison
        // If HTTPS, ports NEED to match. Otherwise, HTTP ports can be different and are same origin
        doPortsPassSameSchemeCheck = port1 !== port2 ? (port1 !== '443' && port2 !== '443') : true
      }

      return doPortsPassSameSchemeCheck && _.isEqual(parsedUrl, relevantProps)
    }
    default:
      return false
  }
}

function urlMatchesPolicy ({ policy, frameUrl, topUrl }: {
  policy: Policy
  frameUrl: string
  topUrl: string
}): boolean {
  debug('url matches policy?', { policy, frameUrl, topUrl })
  if (!policy || !frameUrl || !topUrl) {
    return false
  }

  return urlMatchesPolicyProps({
    policy,
    frameUrl,
    topProps: parseUrlIntoHostProtocolDomainTldPort(topUrl),
  })
}

export function urlOriginsMatch (frameUrl: string, topUrl: string): boolean {
  return urlMatchesPolicy({
    policy: 'same-origin',
    frameUrl,
    topUrl,
  })
}

export const urlSameSiteMatch = (frameUrl: string, topUrl: string): boolean => {
  return urlMatchesPolicy({
    policy: 'schemeful-same-site',
    frameUrl,
    topUrl,
  })
}

/**
 * Returns the policy that will be used for the specified url.
 * @param url - the url to check the policy against.
 * @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined.
 * @returns a Policy string.
 */
export const policyFromConfig = (config: { injectDocumentDomain: boolean }): Policy => {
  return config.injectDocumentDomain ?
    'same-super-domain-origin' :
    'same-origin'
}

declare module 'url' {
  interface UrlWithStringQuery {
    format(): string
  }
}

export function urlMatchesOriginProtectionSpace (urlStr: string, origin: string) {
  const normalizedUrl = addDefaultPort(urlStr).format()
  const normalizedOrigin = addDefaultPort(origin).format()

  return _.startsWith(normalizedUrl, normalizedOrigin)
}

/**
 * Returns the super-domain of a URL
 *
 * The primary driver uses the super-domain origin to allow tests to
 * navigate between subdomains of the same super-domain by setting
 * document.domain to the super-domain
 * @param url - the full absolute url
 * @returns the super domain origin
 * ex: http://www.example.com:8081/my/path -> http://example.com:8081
 */
export function getSuperDomainOrigin (url: string) {
  // @ts-ignore
  const { port, protocol } = new URL(url)

  // super domain origin is comprised of:
  // protocol + superdomain + port (subdomain is not factored in)
  return _.compact([`${protocol}//${getSuperDomain(url)}`, port]).join(':')
}
