import path from "path"
import fs from "fs"
import { transform as babelTransform } from "@babel/core"
import plugin, { ExtractedMessage, ExtractPluginOpts } from "../src/index"
import { mockConsole } from "@lingui/jest-mocks"
import linguiMacroPlugin, {
  type LinguiPluginOpts,
} from "@lingui/babel-plugin-lingui-macro"

const transform = (filename: string) => {
  const rootDir = path.join(__dirname, "fixtures")

  const filePath = path.join(rootDir, filename)
  const code = fs.readFileSync(filePath).toString()

  return transformCode(code, filePath, rootDir)
}

const transformCode = (
  code: string,
  filename = "test-case.js",
  rootDir = "."
) => {
  process.env.LINGUI_CONFIG = path.join(
    __dirname,
    "fixtures",
    "lingui.config.js"
  )
  const messages: ExtractedMessage[] = []

  try {
    const pluginOpts: ExtractPluginOpts = {
      onMessageExtracted(msg: ExtractedMessage) {
        const filename = path.relative(rootDir, msg.origin[0])
        messages.push({
          ...msg,
          origin: [filename, msg.origin[1]],
        })
      },
    }

    babelTransform(code, {
      configFile: false,
      filename,
      plugins: [
        "@babel/plugin-syntax-jsx",
        [
          linguiMacroPlugin,
          {
            extract: true,
          } satisfies LinguiPluginOpts,
        ],
        [plugin, pluginOpts],
      ],
    })
  } finally {
    process.env.LINGUI_CONFIG = null
  }

  return messages
}

function expectNoConsole(cb: () => void) {
  return mockConsole((console) => {
    cb()

    expect(console.warn).not.toBeCalled()
    expect(console.error).not.toBeCalled()
  })
}

describe("@lingui/babel-plugin-extract-messages", function () {
  it("should ignore files without lingui import", () => {
    expectNoConsole(() => {
      const messages = transform("without-lingui.js")
      expect(messages.length).toBe(0)
    })
  })

  it("should extract all messages from JSX files", () => {
    expectNoConsole(() => {
      const messages = transform("jsx-without-macros.js")
      expect(messages).toMatchSnapshot()
    })
  })

  describe("JSX", () => {
    it("Should not rise warning when translation from variable", () => {
      const code = `
import { Trans } from "@lingui/react";

<Trans id={message} />;
<Trans id={message.field} />;
      `
      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages.length).toBe(0)
      })
    })

    it("Should not rise warning when `key` used with macro", () => {
      const code = `
import { Trans } from "@lingui/react/macro";

<Trans context="Context2" key={1}>
  Some message
</Trans>
      `
      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages.length).toBe(1)
      })
    })

    it("Should log error when no ID provided", () => {
      const code = `
import { Trans } from "@lingui/react";

<Trans />;
<Trans message="Missing ID" />;
      `
      mockConsole((console) => {
        const messages = transformCode(code)

        expect(messages.length).toBe(0)
        expect(console.error).not.toBeCalled()
        expect(console.warn).toBeCalledWith(
          expect.stringContaining(`Missing message ID`)
        )
      })
    })
  })

  describe("CallExpression i18n._()", () => {
    it("should extract messages from i18n._ call expressions", () => {
      expectNoConsole(() => {
        const messages = transform("js-call-expression.js")
        expect(messages).toMatchSnapshot()
      })
    })

    it("should extract from member access expressions", () => {
      const code = `
      // member access
      ctx.i18n._("Message")
      
      // member access any depth
      ctx.req.i18n._("Message")
      `
      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages.length).toBe(2)
      })
    })

    it("should not extract from random i18n members", () => {
      const code = `
      i18n.load("Message")
      `
      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages.length).toBe(0)
      })
    })

    it("should not extract if disabled via annotation", () => {
      const code = `
      /* lingui-extract-ignore */
      i18n._("Message")
            
      /* lingui-extract-ignore */
      ctx.i18n._("Message")
      
       /* lingui-extract-ignore */
      ctx.req.i18n._("Message")
      `
      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages.length).toBe(0)
      })
    })

    it("Should not rise warning when translation from variable", () => {
      const code = `
      i18n._(message);
      // member expression
      i18n._(foo.bar);
      // function call
      i18n._(getMessage());
      `

      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages.length).toBe(0)
      })
    })

    it("Should throw error when not a string provided as comment", () => {
      const code = `const msg = i18n._('message.id', {}, {comment: variable})`

      return mockConsole((console) => {
        transformCode(code)

        expect(console.error).not.toBeCalled()
        expect(console.warn).toBeCalledWith(
          expect.stringContaining(
            "Only strings or template literals could be extracted."
          )
        )
      })
    })

    it("Should support extract id from TplLiteral and Concatenation", () => {
      const code = `
      const msg = i18n._(\`message.id\`);
      const msg2 = i18n._("second" + '.' + "id")
      `

      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages[0]).toMatchObject({
          id: "message.id",
        })
        expect(messages[1]).toMatchObject({
          id: "second.id",
        })
      })
    })

    it("Should support string concatenation", () => {
      const code = `const msg = i18n._('message.id', {}, {comment: "first " + "second " + "third"})`

      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages[0]).toMatchObject({
          comment: "first second third",
        })
      })
    })

    it("Should not double extract from call generated by macro and dont rise warnings", () => {
      const code = `import { i18n } from "@lingui/core";
    const msg =
      i18n._(/*i18n*/
        {
          id: "Hello {name}",
          values: {
            name: name,
          },
        });
      `
      expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages.length).toBe(1)
      })
    })
  })

  describe("StringLiteral", () => {
    it("Should extract from marked StringLiteral", () => {
      const code = `const t = /*i18n*/'Message'`

      expectNoConsole(() => {
        const messages = transformCode(code)

        expect(messages[0]).toMatchObject({
          id: "Message",
        })
      })
    })

    it("Should log error when empty StringLiteral marked for extraction", () => {
      const code = `const t = /*i18n*/''`

      return mockConsole((console) => {
        const messages = transformCode(code)

        expect(messages.length).toBe(0)
        expect(console.error).not.toBeCalled()
        expect(console.warn).toBeCalledWith(
          expect.stringContaining(`Empty StringLiteral`)
        )
      })
    })
  })

  describe("MessageDescriptor", () => {
    it("should extract messages from MessageDescriptors", () => {
      expectNoConsole(() => {
        const messages = transform("js-message-descriptor.js")
        expect(messages).toMatchSnapshot()
      })
    })

    it("Should extract id from TemplateLiteral", () => {
      const code = "const msg = /*i18n*/{id: `Message`}"

      expectNoConsole(() => {
        const messages = transformCode(code)

        expect(messages[0]).toMatchObject({
          id: "Message",
        })
      })
    })

    it("Should log error if TemplateLiteral in id has expressions", () => {
      const code = "const msg = /*i18n*/{id: `Hello ${name}`}"

      return mockConsole((console) => {
        const messages = transformCode(code)

        expect(messages.length).toBe(0)

        expect(console.error).not.toBeCalled()
        expect(console.warn).toBeCalledWith(
          expect.stringContaining(
            `Could not extract from template literal with expressions.`
          )
        )
      })
    })

    it("Should log error when no ID provided", () => {
      const code = "const msg = /*i18n*/ {message: `Hello ${name}`}"

      return mockConsole((console) => {
        const messages = transformCode(code)

        expect(messages.length).toBe(0)
        expect(console.error).not.toBeCalled()

        expect(console.warn).toBeCalledWith(
          expect.stringContaining(`Missing message ID`)
        )
      })
    })

    it("Should log error when not a string provided as ID", () => {
      const code = "const msg = /*i18n*/ {id: id}"

      return mockConsole((console) => {
        transformCode(code)

        expect(console.error).not.toBeCalled()
        expect(console.warn).toBeCalledWith(
          expect.stringContaining(
            "Only strings or template literals could be extracted."
          )
        )
      })
    })

    it("Should log error when not a string provided as comment", () => {
      const code = `const msg =  /*i18n*/ {id: "msg.id", comment: variable}`

      return mockConsole((console) => {
        transformCode(code)

        expect(console.error).not.toBeCalled()
        expect(console.warn).toBeCalledWith(
          expect.stringContaining(
            "Only strings or template literals could be extracted."
          )
        )
      })
    })

    it("Should support string concatenation in comment", () => {
      const code = `const msg =  /*i18n*/ {id: "msg.id", comment: "first " + "second " + "third"}`

      return expectNoConsole(() => {
        const messages = transformCode(code)
        expect(messages[0]).toMatchObject({
          comment: "first second third",
        })
      })
    })
  })

  it("should extract all messages from JSX files (macros)", () => {
    return expectNoConsole(() => {
      const messages = transform("jsx-with-macros.js")
      expect(messages).toMatchSnapshot()
    })
  })

  it("should extract Plural messages from JSX files when there's no Trans tag (integration)", () => {
    return expectNoConsole(() => {
      const messages = transform("jsx-without-trans.js")
      expect(messages).toMatchSnapshot()
    })
  })

  it("should extract all messages from JS files (macros)", () => {
    return expectNoConsole(() => {
      const messages = transform("js-with-macros.js")
      expect(messages).toMatchSnapshot()
    })
  })
})
